suneditor 3.0.0-beta.2 → 3.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CONTRIBUTING.md +186 -184
  2. package/LICENSE +21 -21
  3. package/README.md +157 -180
  4. package/dist/suneditor.min.css +1 -1
  5. package/dist/suneditor.min.js +1 -1
  6. package/package.json +126 -123
  7. package/src/assets/design/color.css +131 -121
  8. package/src/assets/design/index.css +3 -3
  9. package/src/assets/design/size.css +37 -35
  10. package/src/assets/design/typography.css +37 -37
  11. package/src/assets/icons/defaultIcons.js +247 -232
  12. package/src/assets/suneditor-contents.css +779 -778
  13. package/src/assets/suneditor.css +43 -35
  14. package/src/core/base/eventHandlers/handler_toolbar.js +135 -135
  15. package/src/core/base/eventHandlers/handler_ww_clipboard.js +56 -56
  16. package/src/core/base/eventHandlers/handler_ww_dragDrop.js +115 -113
  17. package/src/core/base/eventHandlers/handler_ww_key_input.js +1200 -1200
  18. package/src/core/base/eventHandlers/handler_ww_mouse.js +194 -194
  19. package/src/core/base/eventManager.js +1550 -1484
  20. package/src/core/base/history.js +355 -355
  21. package/src/core/class/char.js +163 -162
  22. package/src/core/class/component.js +856 -842
  23. package/src/core/class/format.js +3433 -3422
  24. package/src/core/class/html.js +1927 -1890
  25. package/src/core/class/menu.js +357 -346
  26. package/src/core/class/nodeTransform.js +424 -424
  27. package/src/core/class/offset.js +858 -891
  28. package/src/core/class/selection.js +710 -620
  29. package/src/core/class/shortcuts.js +98 -98
  30. package/src/core/class/toolbar.js +438 -430
  31. package/src/core/class/ui.js +424 -422
  32. package/src/core/class/viewer.js +750 -750
  33. package/src/core/editor.js +1810 -1708
  34. package/src/core/section/actives.js +268 -241
  35. package/src/core/section/constructor.js +1348 -1661
  36. package/src/core/section/context.js +102 -102
  37. package/src/core/section/documentType.js +582 -561
  38. package/src/core/section/options.js +367 -0
  39. package/src/core/util/instanceCheck.js +59 -0
  40. package/src/editorInjector/_classes.js +36 -36
  41. package/src/editorInjector/_core.js +92 -92
  42. package/src/editorInjector/index.js +75 -75
  43. package/src/events.js +634 -622
  44. package/src/helper/clipboard.js +59 -59
  45. package/src/helper/converter.js +586 -564
  46. package/src/helper/dom/domCheck.js +304 -304
  47. package/src/helper/dom/domQuery.js +677 -669
  48. package/src/helper/dom/domUtils.js +618 -557
  49. package/src/helper/dom/index.js +12 -12
  50. package/src/helper/env.js +249 -240
  51. package/src/helper/index.js +25 -25
  52. package/src/helper/keyCodeMap.js +183 -183
  53. package/src/helper/numbers.js +72 -72
  54. package/src/helper/unicode.js +47 -47
  55. package/src/langs/ckb.js +231 -231
  56. package/src/langs/cs.js +231 -231
  57. package/src/langs/da.js +231 -231
  58. package/src/langs/de.js +231 -231
  59. package/src/langs/en.js +230 -230
  60. package/src/langs/es.js +231 -231
  61. package/src/langs/fa.js +231 -231
  62. package/src/langs/fr.js +231 -231
  63. package/src/langs/he.js +231 -231
  64. package/src/langs/hu.js +230 -230
  65. package/src/langs/index.js +28 -28
  66. package/src/langs/it.js +231 -231
  67. package/src/langs/ja.js +230 -230
  68. package/src/langs/km.js +230 -230
  69. package/src/langs/ko.js +230 -230
  70. package/src/langs/lv.js +231 -231
  71. package/src/langs/nl.js +231 -231
  72. package/src/langs/pl.js +231 -231
  73. package/src/langs/pt_br.js +231 -231
  74. package/src/langs/ro.js +231 -231
  75. package/src/langs/ru.js +231 -231
  76. package/src/langs/se.js +231 -231
  77. package/src/langs/tr.js +231 -231
  78. package/src/langs/uk.js +231 -231
  79. package/src/langs/ur.js +231 -231
  80. package/src/langs/zh_cn.js +231 -231
  81. package/src/modules/ApiManager.js +191 -191
  82. package/src/modules/Browser.js +669 -667
  83. package/src/modules/ColorPicker.js +364 -362
  84. package/src/modules/Controller.js +474 -454
  85. package/src/modules/Figure.js +1620 -1617
  86. package/src/modules/FileManager.js +359 -359
  87. package/src/modules/HueSlider.js +577 -565
  88. package/src/modules/Modal.js +346 -346
  89. package/src/modules/ModalAnchorEditor.js +643 -643
  90. package/src/modules/SelectMenu.js +549 -549
  91. package/src/modules/_DragHandle.js +17 -17
  92. package/src/modules/index.js +14 -14
  93. package/src/plugins/browser/audioGallery.js +83 -83
  94. package/src/plugins/browser/fileBrowser.js +103 -103
  95. package/src/plugins/browser/fileGallery.js +83 -83
  96. package/src/plugins/browser/imageGallery.js +81 -81
  97. package/src/plugins/browser/videoGallery.js +103 -103
  98. package/src/plugins/command/blockquote.js +61 -60
  99. package/src/plugins/command/exportPDF.js +134 -134
  100. package/src/plugins/command/fileUpload.js +456 -456
  101. package/src/plugins/command/list_bulleted.js +149 -148
  102. package/src/plugins/command/list_numbered.js +152 -151
  103. package/src/plugins/dropdown/align.js +157 -155
  104. package/src/plugins/dropdown/backgroundColor.js +108 -104
  105. package/src/plugins/dropdown/font.js +141 -137
  106. package/src/plugins/dropdown/fontColor.js +109 -105
  107. package/src/plugins/dropdown/formatBlock.js +170 -178
  108. package/src/plugins/dropdown/hr.js +152 -152
  109. package/src/plugins/dropdown/layout.js +83 -83
  110. package/src/plugins/dropdown/lineHeight.js +131 -130
  111. package/src/plugins/dropdown/list.js +123 -122
  112. package/src/plugins/dropdown/paragraphStyle.js +138 -138
  113. package/src/plugins/dropdown/table.js +4110 -4000
  114. package/src/plugins/dropdown/template.js +83 -83
  115. package/src/plugins/dropdown/textStyle.js +149 -149
  116. package/src/plugins/field/mention.js +242 -242
  117. package/src/plugins/index.js +120 -120
  118. package/src/plugins/input/fontSize.js +414 -410
  119. package/src/plugins/input/pageNavigator.js +71 -70
  120. package/src/plugins/modal/audio.js +677 -677
  121. package/src/plugins/modal/drawing.js +537 -531
  122. package/src/plugins/modal/embed.js +886 -886
  123. package/src/plugins/modal/image.js +1377 -1376
  124. package/src/plugins/modal/link.js +248 -240
  125. package/src/plugins/modal/math.js +563 -563
  126. package/src/plugins/modal/video.js +1226 -1226
  127. package/src/plugins/popup/anchor.js +224 -222
  128. package/src/suneditor.js +114 -107
  129. package/src/themes/dark.css +132 -122
  130. package/src/typedef.js +132 -130
  131. package/types/assets/icons/defaultIcons.d.ts +8 -0
  132. package/types/core/base/eventManager.d.ts +29 -4
  133. package/types/core/class/char.d.ts +2 -1
  134. package/types/core/class/component.d.ts +1 -2
  135. package/types/core/class/format.d.ts +8 -1
  136. package/types/core/class/html.d.ts +8 -0
  137. package/types/core/class/menu.d.ts +8 -0
  138. package/types/core/class/offset.d.ts +24 -26
  139. package/types/core/class/selection.d.ts +2 -0
  140. package/types/core/class/toolbar.d.ts +6 -0
  141. package/types/core/class/ui.d.ts +1 -1
  142. package/types/core/editor.d.ts +34 -12
  143. package/types/core/section/constructor.d.ts +5 -638
  144. package/types/core/section/documentType.d.ts +12 -2
  145. package/types/core/section/options.d.ts +740 -0
  146. package/types/core/util/instanceCheck.d.ts +50 -0
  147. package/types/editorInjector/_core.d.ts +5 -5
  148. package/types/editorInjector/index.d.ts +2 -2
  149. package/types/events.d.ts +2 -0
  150. package/types/helper/converter.d.ts +9 -0
  151. package/types/helper/dom/domQuery.d.ts +5 -5
  152. package/types/helper/dom/domUtils.d.ts +8 -0
  153. package/types/helper/env.d.ts +6 -1
  154. package/types/helper/index.d.ts +4 -1
  155. package/types/index.d.ts +122 -120
  156. package/types/langs/_Lang.d.ts +194 -194
  157. package/types/modules/ColorPicker.d.ts +5 -1
  158. package/types/modules/Controller.d.ts +8 -4
  159. package/types/modules/Figure.d.ts +2 -1
  160. package/types/modules/HueSlider.d.ts +4 -1
  161. package/types/modules/SelectMenu.d.ts +1 -1
  162. package/types/plugins/command/blockquote.d.ts +1 -0
  163. package/types/plugins/command/list_bulleted.d.ts +1 -0
  164. package/types/plugins/command/list_numbered.d.ts +1 -0
  165. package/types/plugins/dropdown/align.d.ts +1 -0
  166. package/types/plugins/dropdown/backgroundColor.d.ts +1 -0
  167. package/types/plugins/dropdown/font.d.ts +1 -0
  168. package/types/plugins/dropdown/fontColor.d.ts +1 -0
  169. package/types/plugins/dropdown/formatBlock.d.ts +3 -2
  170. package/types/plugins/dropdown/lineHeight.d.ts +1 -0
  171. package/types/plugins/dropdown/list.d.ts +1 -0
  172. package/types/plugins/dropdown/table.d.ts +6 -0
  173. package/types/plugins/input/fontSize.d.ts +1 -0
  174. package/types/plugins/modal/drawing.d.ts +4 -0
  175. package/types/plugins/modal/link.d.ts +32 -15
  176. package/types/suneditor.d.ts +13 -9
  177. package/types/typedef.d.ts +8 -0
@@ -1,886 +1,886 @@
1
- import EditorInjector from '../../editorInjector';
2
- import { Modal, Figure } from '../../modules';
3
- import { dom, numbers, env, keyCodeMap } from '../../helper';
4
- const { NO_EVENT } = env;
5
-
6
- /**
7
- * @typedef {import('../../events').ProcessInfo} ProcessInfo_embed
8
- */
9
-
10
- /**
11
- * @typedef {import('../../modules/Figure').FigureControls} FigureControls_embed
12
- */
13
-
14
- /**
15
- * @typedef {Object} EmbedPluginOptions
16
- * @property {boolean} [canResize=true] - Whether the embed 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 embed element (numeric value or with unit).
19
- * @property {string} [defaultHeight] - The default height of the embed element (numeric value or with unit).
20
- * @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing.
21
- * @property {string} [uploadUrl] - The URL for file uploads.
22
- * @property {Object<string, string>} [uploadHeaders] - Headers to include in file upload requests.
23
- * @property {number} [uploadSizeLimit] - The total file upload size limit in bytes.
24
- * @property {number} [uploadSingleSizeLimit] - The single file upload size limit in bytes.
25
- * @property {Object<string, string>} [iframeTagAttributes] - Additional attributes to set on the iframe tag.
26
- * @property {string} [query_youtube] - YouTube query parameter.
27
- * @property {string} [query_vimeo] - Vimeo query parameter.
28
- * @property {Array<RegExp>} [urlPatterns] - Additional URL patterns for embed.
29
- * @property {Object<string, {pattern: RegExp, action: (url: string) => string, tag: string}>} [embedQuery] - Custom query objects for additional embedding services.
30
- * Example :
31
- * {
32
- * facebook: {
33
- * pattern: /(?:https?:\/\/)?(?:www\.)?(?:facebook\.com)\/(.+)/i,
34
- * action: (url) => {
35
- * return `https://www.facebook.com/plugins/post.php?href=${encodeURIComponent(url)}&show_text=true&width=500`;
36
- * },
37
- * tag: 'iframe'
38
- * },
39
- * twitter: {
40
- * pattern: /(?:https?:\/\/)?(?:www\.)?(?:twitter\.com)\/(status|embed)\/(.+)/i,
41
- * action: (url) => {
42
- * return `https://platform.twitter.com/embed/Tweet.html?url=${encodeURIComponent(url)}`;
43
- * },
44
- * tag: 'iframe'
45
- * },
46
- * // Additional services...
47
- * }
48
- * @property {FigureControls_embed} [controls] - Figure controls.
49
- */
50
-
51
- /**
52
- * @class
53
- * @description Embed modal plugin.
54
- * - This plugin provides a modal interface for embedding external content (e.g., videos, iframes) into the editor.
55
- */
56
- class Embed extends EditorInjector {
57
- static key = 'embed';
58
- static type = 'modal';
59
- static className = '';
60
- /**
61
- * @this {Embed}
62
- * @param {HTMLElement} node - The node to check.
63
- * @returns {HTMLElement|null} Returns a node if the node is a valid component.
64
- */
65
- static component(node) {
66
- let src = '';
67
- if (dom.check.isIFrame(node)) src = node.src;
68
- if (/^DIV$/i.test(node?.nodeName) && dom.check.isIFrame(node.firstElementChild)) src = node.firstElementChild.src;
69
-
70
- if (src) {
71
- return this.checkContentType(src) ? node : null;
72
- }
73
- return null;
74
- }
75
-
76
- /**
77
- * @constructor
78
- * @param {__se__EditorCore} editor - The root editor instance
79
- * @param {EmbedPluginOptions} pluginOptions
80
- */
81
- constructor(editor, pluginOptions) {
82
- // plugin bisic properties
83
- super(editor);
84
- this.title = this.lang.embed;
85
- this.icon = 'embed';
86
-
87
- // define plugin options
88
- this.pluginOptions = {
89
- canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
90
- showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
91
- defaultWidth: !pluginOptions.defaultWidth || !numbers.get(pluginOptions.defaultWidth, 0) ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
92
- defaultHeight: !pluginOptions.defaultHeight || !numbers.get(pluginOptions.defaultHeight, 0) ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
93
- percentageOnlySize: !!pluginOptions.percentageOnlySize,
94
- uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
95
- uploadHeaders: pluginOptions.uploadHeaders || null,
96
- uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
97
- uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
98
- iframeTagAttributes: pluginOptions.iframeTagAttributes || null,
99
- query_youtube: pluginOptions.query_youtube || '',
100
- query_vimeo: pluginOptions.query_vimeo || ''
101
- };
102
-
103
- // create HTML
104
- const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px';
105
- const modalEl = CreateHTML_modal(editor, this.pluginOptions);
106
- const figureControls = pluginOptions.controls || !this.pluginOptions.canResize ? [['align', 'edit', 'copy', 'remove']] : [['resize_auto,75,50', 'align', 'edit', 'revert', 'copy', 'remove']];
107
-
108
- // show align
109
- if (!figureControls.some((subArray) => subArray.includes('align'))) modalEl.figureAlignBtn.style.display = 'none';
110
-
111
- // modules
112
- this.modal = new Modal(this, modalEl.html);
113
- this.figure = new Figure(this, figureControls, { sizeUnit: sizeUnit });
114
-
115
- // members
116
- this.fileModalWrapper = modalEl.fileModalWrapper;
117
- this.embedInput = modalEl.embedInput;
118
- this.focusElement = this.embedInput;
119
- this.previewSrc = modalEl.previewSrc;
120
- this._linkValue = '';
121
- this._align = 'none';
122
- this._defaultSizeX = this.pluginOptions.defaultWidth;
123
- this._defaultSizeY = this.pluginOptions.defaultHeight;
124
- this.sizeUnit = sizeUnit;
125
- this.proportion = null;
126
- this.inputX = null;
127
- this.inputY = null;
128
- this._element = null;
129
- this._cover = null;
130
- this._container = null;
131
- this._ratio = { w: 1, h: 1 };
132
- this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
133
- this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
134
- this._resizing = this.pluginOptions.canResize;
135
- this._onlyPercentage = this.pluginOptions.percentageOnlySize;
136
- this._nonResizing = !this._resizing || !this.pluginOptions.showHeightInput || this._onlyPercentage;
137
- this.query = {
138
- facebook: {
139
- pattern: /(?:https?:\/\/)?(?:www\.)?(?:facebook\.com)\/(.+)/i,
140
- action: (url) => {
141
- return `https://www.facebook.com/plugins/post.php?href=${encodeURIComponent(url)}&show_text=true&width=500`;
142
- },
143
- tag: 'iframe'
144
- },
145
- twitter: {
146
- pattern: /(?:https?:\/\/)?(?:www\.)?(?:twitter\.com)\/(status|embed)\/(.+)/i,
147
- action: (url) => {
148
- return `https://platform.twitter.com/embed/Tweet.html?url=${encodeURIComponent(url)}`;
149
- },
150
- tag: 'iframe'
151
- },
152
- instagram: {
153
- pattern: /(?:https?:\/\/)?(?:www\.)?(?:instagram\.com)\/p\/(.+)/i,
154
- action: (url) => {
155
- const postId = url.match(this.query.instagram.pattern)[1];
156
- return `https://www.instagram.com/p/${postId}/embed`;
157
- },
158
- tag: 'iframe'
159
- },
160
- linkedin: {
161
- pattern: /(?:https?:\/\/)?(?:www\.)?(?:linkedin\.com)\/(.+)\/(.+)/i,
162
- action: (url) => {
163
- return `https://www.linkedin.com/embed/feed/update/${encodeURIComponent(url.split('/').pop())}`;
164
- },
165
- tag: 'iframe'
166
- },
167
- pinterest: {
168
- pattern: /(?:https?:\/\/)?(?:www\.)?(?:pinterest\.com)\/pin\/(.+)/i,
169
- action: (url) => {
170
- const pinId = url.match(this.query.pinterest.pattern)[1];
171
- return `https://assets.pinterest.com/ext/embed.html?id=${pinId}`;
172
- },
173
- tag: 'iframe'
174
- },
175
- spotify: {
176
- pattern: /(?:https?:\/\/)?(?:open\.)?(?:spotify\.com)\/(track|album|playlist|show|episode)\/(.+)/i,
177
- action: (url) => {
178
- const match = url.match(this.query.spotify.pattern);
179
- const type = match[1];
180
- const id = match[2];
181
- return `https://open.spotify.com/embed/${type}/${id}`;
182
- },
183
- tag: 'iframe'
184
- },
185
- codepen: {
186
- pattern: /(?:https?:\/\/)?(?:www\.)?(?:codepen\.io)\/(.+)\/pen\/(.+)/i,
187
- action: (url) => {
188
- const [, user, penId] = url.match(this.query.codepen.pattern);
189
- return `https://codepen.io/${user}/embed/${penId}`;
190
- },
191
- tag: 'iframe'
192
- },
193
- ...pluginOptions.embedQuery
194
- };
195
-
196
- const urlPatterns = [];
197
- for (const key in this.query) {
198
- urlPatterns.push(this.query[key].pattern);
199
- }
200
- this.urlPatterns = urlPatterns.concat(pluginOptions.urlPatterns || []);
201
-
202
- // init
203
- this.eventManager.addEvent(this.embedInput, 'input', this.#OnLinkPreview.bind(this));
204
-
205
- if (this._resizing) {
206
- this.proportion = modalEl.proportion;
207
- this.inputX = modalEl.inputX;
208
- this.inputY = modalEl.inputY;
209
- this.inputX.value = this.pluginOptions.defaultWidth;
210
- this.inputY.value = this.pluginOptions.defaultHeight;
211
-
212
- this.eventManager.addEvent(this.inputX, 'keyup', this.#OnInputSize.bind(this, 'x'));
213
- this.eventManager.addEvent(this.inputY, 'keyup', this.#OnInputSize.bind(this, 'y'));
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.
221
- */
222
- open() {
223
- this.modal.open();
224
- }
225
-
226
- /**
227
- * @editorMethod Modules.Controller(Figure)
228
- * @description Executes the method that is called when a target component is edited.
229
- */
230
- edit() {
231
- this.modal.open();
232
- }
233
-
234
- /**
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)."
238
- */
239
- on(isUpdate) {
240
- if (!isUpdate && this._resizing) {
241
- this.inputX.value = this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
242
- this.inputY.value = this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
243
- this.proportion.disabled = true;
244
- }
245
- }
246
-
247
- /**
248
- * @editorMethod Modules.Modal
249
- * @description This function is called when a form within a modal window is "submit".
250
- * @returns {Promise<boolean>} Success / failure
251
- */
252
- async modalAction() {
253
- this._align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_embed_radio"]:checked')).value;
254
-
255
- let result = false;
256
- if (this._linkValue.length > 0) {
257
- result = await this.submitSRC(this._linkValue);
258
- }
259
-
260
- if (result) this._w.setTimeout(this.component.select.bind(this.component, this._element, Embed.key), 0);
261
-
262
- return result;
263
- }
264
-
265
- /**
266
- * @editorMethod Editor.core
267
- * @description This method is used to validate and preserve the format of the component within the editor.
268
- * - It ensures that the structure and attributes of the element are maintained and secure.
269
- * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
270
- * - If the element isn't properly contained, a new container is created to retain the format.
271
- * @returns {{query: string, method: (element: HTMLIFrameElement) => void}} The format retention object containing the query and method to process the element.
272
- * - query: The selector query to identify the relevant elements (in this case, 'audio').
273
- * - method:The function to execute on the element to validate and preserve its format.
274
- * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
275
- */
276
- retainFormat() {
277
- return {
278
- query: 'iframe',
279
- method: async (element) => {
280
- if (!this.checkContentType(element.src)) return;
281
-
282
- const figureInfo = Figure.GetContainer(element);
283
- if (figureInfo && figureInfo.container && figureInfo.cover) return;
284
-
285
- this._ready(element);
286
- const line = this.format.getLine(element);
287
- if (line) this._align = line.style.textAlign || line.style.float;
288
-
289
- this._update(element);
290
- }
291
- };
292
- }
293
-
294
- /**
295
- * @editorMethod Modules.Modal
296
- * @description This function is called before the modal window is opened, but before it is closed.
297
- */
298
- init() {
299
- Modal.OnChangeFile(this.fileModalWrapper, []);
300
- this._linkValue = this.previewSrc.textContent = this.embedInput.value = '';
301
-
302
- /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_embed_radio"][value="none"]')).checked = true;
303
- this._ratio = { w: 1, h: 1 };
304
- this._nonResizing = false;
305
-
306
- if (this._resizing) {
307
- this.inputX.value = this.pluginOptions.defaultWidth === this._defaultSizeX ? '' : this.pluginOptions.defaultWidth;
308
- this.inputY.value = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
309
- this.proportion.checked = false;
310
- this.proportion.disabled = true;
311
- }
312
- }
313
-
314
- /**
315
- * @editorMethod Editor.Component
316
- * @description Executes the method that is called when a component of a plugin is selected.
317
- * @param {HTMLElement} target Target component element
318
- */
319
- select(target) {
320
- this._ready(target);
321
- }
322
-
323
- /**
324
- * @private
325
- * @description Prepares the component for selection.
326
- * - Ensures that the controller is properly positioned and initialized.
327
- * - Prevents duplicate event handling if the component is already selected.
328
- * @param {HTMLElement} target - The selected element.
329
- */
330
- _ready(target) {
331
- if (!target) return;
332
- const figureInfo = this.figure.open(target, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: false });
333
-
334
- this._element = target;
335
- this._cover = figureInfo.cover;
336
- this._container = figureInfo.container;
337
- this._caption = figureInfo.caption;
338
- this._align = figureInfo.align;
339
- target.style.float = '';
340
-
341
- this._origin_w = String(figureInfo.originWidth || figureInfo.w || '');
342
- this._origin_h = String(figureInfo.originHeight || figureInfo.h || '');
343
-
344
- /** @type {HTMLInputElement} */
345
- const activeAlign = this.modal.form.querySelector('input[name="suneditor_embed_radio"][value="' + this._align + '"]') || this.modal.form.querySelector('input[name="suneditor_embed_radio"][value="none"]');
346
- activeAlign.checked = true;
347
-
348
- if (!this._resizing) return;
349
-
350
- const percentageRotation = this._onlyPercentage && this.figure.isVertical;
351
- let w = percentageRotation ? '' : figureInfo.width;
352
- if (this._onlyPercentage) {
353
- w = numbers.get(w, 2);
354
- if (w > 100) w = 100;
355
- }
356
-
357
- this.inputX.value = String(w === 'auto' ? '' : w);
358
-
359
- if (!this._onlyPercentage) {
360
- const h = percentageRotation ? '' : figureInfo.height;
361
- this.inputY.value = String(h === 'auto' ? '' : h);
362
- }
363
-
364
- this.proportion.checked = true;
365
- this.inputX.disabled = percentageRotation ? true : false;
366
- this.inputY.disabled = percentageRotation ? true : false;
367
- this.proportion.disabled = percentageRotation ? true : false;
368
-
369
- this._ratio = this.proportion.checked
370
- ? figureInfo.ratio
371
- : {
372
- w: 1,
373
- h: 1
374
- };
375
- }
376
-
377
- /**
378
- * @editorMethod Editor.Component
379
- * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
380
- * @param {HTMLElement} target Target element
381
- * @returns {Promise<void>}
382
- */
383
- async destroy(target) {
384
- const targetEl = target || this._element;
385
- const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
386
- const focusEl = container.previousElementSibling || container.nextElementSibling;
387
- const emptyDiv = container.parentNode;
388
-
389
- const message = await this.triggerEvent('onEmbedDeleteBefore', { element: targetEl, container, align: this._align, url: this._linkValue });
390
- if (message === false) return;
391
-
392
- dom.utils.removeItem(container);
393
- this.init();
394
-
395
- if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
396
- this.nodeTransform.removeAllParents(
397
- emptyDiv,
398
- function (current) {
399
- return current.childNodes.length === 0;
400
- },
401
- null
402
- );
403
- }
404
-
405
- // focus
406
- this.editor.focusEdge(focusEl);
407
- this.history.push(false);
408
- }
409
-
410
- /**
411
- * @description Checks if the given URL matches any of the defined URL patterns.
412
- * @param {string} url - The URL to check.
413
- * @returns {boolean} True if the URL matches a known pattern; otherwise, false.
414
- */
415
- checkContentType(url) {
416
- url = url?.toLowerCase() || '';
417
- if (this.urlPatterns.some((pattern) => pattern.test(url))) {
418
- return true;
419
- }
420
-
421
- return false;
422
- }
423
-
424
- /**
425
- * @description Finds and processes the URL for embedding by matching it against known service patterns.
426
- * @param {string} url - The original URL.
427
- * @returns {{origin: string, url: string, tag: string}|null} An object containing the original URL, the processed URL, and the tag type (e.g., 'iframe'),
428
- * or null if no matching pattern is found.
429
- */
430
- findProcessUrl(url) {
431
- const query = this.query;
432
- for (const key in query) {
433
- const service = query[key];
434
- if (service.pattern.test(url)) {
435
- return {
436
- origin: url,
437
- url: service.action(url),
438
- tag: service.tag
439
- };
440
- }
441
- }
442
-
443
- return null;
444
- }
445
-
446
- /**
447
- * @description Processes the provided source (URL or embed code) and submits it for embedding.
448
- * - It parses the input, triggers any necessary events, and creates or updates the embed component.
449
- * @param {string} [src] - The embed source. If not provided, uses the internally stored link value.
450
- * @returns {Promise<boolean>} A promise that resolves to true on success or false on failure.
451
- */
452
- async submitSRC(src) {
453
- if (!src) src = this._linkValue;
454
- if (!src) return false;
455
-
456
- let embedInfo = null;
457
- if (/^<iframe\s|^<blockquote\s/i.test(src)) {
458
- const embedDOM = new DOMParser().parseFromString(src, 'text/html').body.children;
459
- if (embedDOM.length === 0) return false;
460
- embedInfo = { children: embedDOM, ...this._getInfo(), process: null };
461
- } else {
462
- const processUrl = this.findProcessUrl(src);
463
- if (!processUrl) return false;
464
- src = processUrl.url;
465
- embedInfo = { url: src, ...this._getInfo(), process: processUrl };
466
- }
467
-
468
- const handler = function (infos, newInfos) {
469
- infos = newInfos || infos;
470
- this._create(infos.process, infos.url, infos.children, infos.inputWidth, infos.inputHeight, infos.align, infos.isUpdate);
471
- }.bind(this, embedInfo);
472
- // se-ts-ignore
473
- void this._create;
474
-
475
- const result = await this.triggerEvent('onEmbedInputBefore', {
476
- ...embedInfo,
477
- handler
478
- });
479
-
480
- if (result === undefined) return true;
481
- if (result === false) return false;
482
- if (result !== null && typeof result === 'object') handler(result);
483
-
484
- if (result === true || result === NO_EVENT) handler(null);
485
-
486
- return true;
487
- }
488
-
489
- /**
490
- * @private
491
- * @description Creates an iframe element for embedding external content.
492
- * @returns {HTMLIFrameElement} The created iframe element.
493
- */
494
- _createIframeTag() {
495
- /** @type {HTMLIFrameElement} */
496
- const iframeTag = dom.utils.createElement('IFRAME');
497
- this._setIframeAttrs(iframeTag);
498
- return iframeTag;
499
- }
500
-
501
- /**
502
- * @private
503
- * @description Creates an blockquote element for embedding external content.
504
- * @returns {HTMLElement} The created iframe element.
505
- */
506
- _createEmbedTag() {
507
- const quoteTag = dom.utils.createElement('BLOCKQUOTE');
508
- return quoteTag;
509
- }
510
-
511
- /**
512
- * @private
513
- * @description Creates an embed component (iframe or blockquote) and inserts it into the editor.
514
- * @param {?ProcessInfo_embed} process - Processed embed information.
515
- * @param {?string} src - The source URL.
516
- * @param {?Node[]} children - The embed elements.
517
- * @param {string} width - The width of the embed component.
518
- * @param {string} height - The height of the embed component.
519
- * @param {string} align - The alignment of the embed component.
520
- * @param {boolean} isUpdate - Whether this is an update to an existing embed component.
521
- */
522
- _create(process, src, children, width, height, align, isUpdate) {
523
- let oFrame = null;
524
- let cover = null;
525
- let container = null;
526
- let scriptTag = null;
527
-
528
- /** update */
529
- if (isUpdate) {
530
- oFrame = this._element;
531
- if (oFrame.src !== src) {
532
- const processUrl = this.findProcessUrl(src);
533
- if (/^iframe$/i.test(processUrl?.tag) && !/^iframe$/i.test(oFrame.nodeName)) {
534
- const newTag = this._createIframeTag();
535
- newTag.src = src;
536
- oFrame.replaceWith(newTag);
537
- oFrame = newTag;
538
- } else if (/^blockquote$/i.test(processUrl?.tag) && !/^blockquote$/i.test(oFrame.nodeName)) {
539
- const newTag = this._createEmbedTag();
540
- newTag.setAttribute('src', src);
541
- oFrame.replaceWith(newTag);
542
- oFrame = newTag;
543
- } else {
544
- oFrame.src = src;
545
- }
546
- }
547
- container = this._container;
548
- cover = dom.query.getParentElement(oFrame, 'FIGURE');
549
- } else {
550
- /** create */
551
- if (process) {
552
- oFrame = this._createIframeTag();
553
- oFrame.src = src;
554
- const figure = Figure.CreateContainer(oFrame, 'se-embed-container');
555
- cover = figure.cover;
556
- container = figure.container;
557
- } else {
558
- oFrame = children[0];
559
- const figure = Figure.CreateContainer(oFrame, 'se-embed-container');
560
- cover = figure.cover;
561
- container = figure.container;
562
-
563
- let chd = null;
564
- let index = 0;
565
- while ((chd = /** @type {Element} */ (children[index]))) {
566
- if (/^script$/i.test(chd.nodeName)) {
567
- scriptTag = dom.utils.createElement('script', { src: chd.getAttribute('src'), async: 'true' }, null);
568
- index++;
569
- continue;
570
- }
571
- cover.appendChild(chd);
572
- }
573
- }
574
- }
575
-
576
- /** rendering */
577
- this._element = oFrame;
578
- this._cover = cover;
579
- this._container = container;
580
- this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
581
-
582
- width = width || this._defaultSizeX;
583
- height = height || this._defaultSizeY;
584
- const size = this.figure.getSize(oFrame);
585
- const inputUpdate = size.w !== width || size.h !== height;
586
- const changeSize = !isUpdate || inputUpdate;
587
-
588
- // set size
589
- if (changeSize) {
590
- this._applySize(width, height);
591
- }
592
-
593
- // align
594
- this.figure.setAlign(oFrame, align);
595
-
596
- if (!isUpdate) {
597
- this.component.insert(container, { skipCharCount: false, skipSelection: true, skipHistory: true });
598
-
599
- if (scriptTag) {
600
- try {
601
- this.history.pause();
602
-
603
- scriptTag.onload = () => {
604
- dom.utils.removeItem(scriptTag);
605
- scriptTag = null;
606
- };
607
- cover.appendChild(scriptTag);
608
-
609
- const observer = new MutationObserver((mutations) => {
610
- for (const mutation of mutations) {
611
- if (mutation.type === 'childList') {
612
- if (!oFrame.parentElement) {
613
- this.history.resume();
614
- this.history.push(false);
615
- observer.disconnect();
616
- break;
617
- }
618
- }
619
- }
620
- });
621
-
622
- observer.observe(this.editor.frameContext.get('wysiwyg'), {
623
- subtree: true,
624
- childList: true
625
- });
626
- } catch (e) {
627
- this.history.resume();
628
- console.warn('[SUNEDITOR] Embed tag script load error.', e);
629
- }
630
- }
631
-
632
- if (!this.options.get('componentAutoSelect')) {
633
- const line = this.format.addLine(container, null);
634
- if (line) this.selection.setRange(line, 0, line, 0);
635
- }
636
- return;
637
- }
638
-
639
- if (this._resizing && changeSize && this.figure.isVertical) this.figure.setTransform(oFrame, width, height, 0);
640
- if (!scriptTag) this.history.push(false);
641
- }
642
-
643
- /**
644
- * @private
645
- * @description Updates an existing embed component within the editor.
646
- * @param {HTMLIFrameElement} oFrame - The existing embed element to be updated.
647
- */
648
- _update(oFrame) {
649
- if (!oFrame) return;
650
-
651
- this._setIframeAttrs(oFrame);
652
-
653
- let existElement = this.format.isBlock(oFrame.parentNode) || dom.check.isWysiwygFrame(oFrame.parentNode) ? oFrame : this.format.getLine(oFrame) || oFrame;
654
-
655
- const prevFrame = oFrame;
656
- oFrame = /** @type {HTMLIFrameElement} */ (oFrame.cloneNode(true));
657
- const figure = Figure.CreateContainer(oFrame, 'se-embed-container');
658
- const container = figure.container;
659
-
660
- // size
661
- this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
662
- const size = (oFrame.getAttribute('data-se-size') || ',').split(',');
663
- this._applySize(size[0] || prevFrame.style.width || prevFrame.width || '', size[1] || prevFrame.style.height || prevFrame.height || '');
664
-
665
- // align
666
- const format = this.format.getLine(prevFrame);
667
- if (format) this._align = format.style.textAlign || format.style.float;
668
- this.figure.setAlign(oFrame, this._align);
669
-
670
- if (dom.query.getParentElement(prevFrame, dom.check.isExcludeFormat)) {
671
- prevFrame.replaceWith(container);
672
- } else if (dom.check.isListCell(existElement)) {
673
- const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
674
- existElement.insertBefore(container, refer);
675
- dom.utils.removeItem(prevFrame);
676
- this.nodeTransform.removeEmptyNode(refer, null, true);
677
- } else if (this.format.isLine(existElement)) {
678
- const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
679
- existElement = this.nodeTransform.split(existElement, refer);
680
- existElement.parentNode.insertBefore(container, existElement);
681
- dom.utils.removeItem(prevFrame);
682
- this.nodeTransform.removeEmptyNode(existElement, null, true);
683
- } else {
684
- /** @type {Element} */ (existElement).replaceWith(container);
685
- }
686
-
687
- return oFrame;
688
- }
689
-
690
- /**
691
- * @private
692
- * @description Applies width and height to the embed component.
693
- * @param {string|number} w - The width to apply.
694
- * @param {string|number} h - The height to apply.
695
- */
696
- _applySize(w, h) {
697
- if (!w) w = this.inputX?.value || this.pluginOptions.defaultWidth;
698
- if (!h) h = this.inputY?.value || this.pluginOptions.defaultHeight;
699
- if (this._onlyPercentage) {
700
- if (!w) w = '100%';
701
- else if (/%$/.test(w + '')) w += '%';
702
- }
703
- this.figure.setSize(w, h);
704
- }
705
-
706
- /**
707
- * @private
708
- * @description Retrieves embed component size and alignment information.
709
- * @returns {{inputWidth: string, inputHeight: string, align: string, isUpdate: boolean, element: Element}} An object containing
710
- * - inputWidth : The width of the embed component.
711
- * - inputHeight : The height of the embed component.
712
- * - align : The alignment of the embed component.
713
- * - isUpdate : Whether the component is being updated.
714
- * - element : The target element.
715
- */
716
- _getInfo() {
717
- return {
718
- inputWidth: this.inputX?.value || '',
719
- inputHeight: this.inputY?.value || '',
720
- align: this._align,
721
- isUpdate: this.modal.isUpdate,
722
- element: this._element
723
- };
724
- }
725
-
726
- /**
727
- * @private
728
- * @description Sets default attributes for an iframe element.
729
- * @param {HTMLIFrameElement} element - The iframe element to modify.
730
- */
731
- _setIframeAttrs(element) {
732
- element.frameBorder = '0';
733
- element.allowFullscreen = true;
734
-
735
- const attrs = this.pluginOptions.iframeTagAttributes;
736
- if (!attrs) return;
737
-
738
- for (const key in attrs) {
739
- element.setAttribute(key, attrs[key]);
740
- }
741
- }
742
-
743
- /**
744
- * @description Handles link preview input changes.
745
- * @param {InputEvent} e - Event object
746
- */
747
- #OnLinkPreview(e) {
748
- /** @type {HTMLInputElement} */
749
- const eventTarget = dom.query.getEventTarget(e);
750
- const value = eventTarget.value.trim();
751
- if (/^<iframe.*\/iframe>$/.test(value)) {
752
- this._linkValue = value;
753
- this.previewSrc.textContent = '<IFrame :src=".."></IFrame>';
754
- } else {
755
- this._linkValue = this.previewSrc.textContent = !value
756
- ? ''
757
- : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
758
- ? this.options.get('defaultUrlProtocol') + value
759
- : !value.includes('://')
760
- ? '/' + value
761
- : value;
762
- }
763
- }
764
-
765
- #OnClickRevert() {
766
- if (this._onlyPercentage) {
767
- this.inputX.value = Number(this._origin_w) > 100 ? '100' : this._origin_w;
768
- } else {
769
- this.inputX.value = this._origin_w;
770
- this.inputY.value = this._origin_h;
771
- }
772
- }
773
-
774
- /**
775
- * @param {"x"|"y"} xy - x or y
776
- * @param {KeyboardEvent} e - Event object
777
- */
778
- #OnInputSize(xy, e) {
779
- if (keyCodeMap.isSpace(e.code)) {
780
- e.preventDefault();
781
- return;
782
- }
783
-
784
- /** @type {HTMLInputElement} */
785
- const eventTarget = dom.query.getEventTarget(e);
786
-
787
- if (xy === 'x' && this._onlyPercentage && Number(eventTarget.value) > 100) {
788
- eventTarget.value = '100';
789
- } else if (this.proportion.checked) {
790
- const ratioSize = Figure.CalcRatio(this.inputX.value, this.inputY.value, this.sizeUnit, this._ratio);
791
- if (xy === 'x') {
792
- this.inputY.value = String(ratioSize.h);
793
- } else {
794
- this.inputX.value = String(ratioSize.w);
795
- }
796
- }
797
- }
798
- }
799
-
800
- /**
801
- * @param {__se__EditorCore} editor Editor instance
802
- * @param {*} pluginOptions
803
- * @returns {{
804
- * html: HTMLElement,
805
- * figureAlignBtn: HTMLButtonElement,
806
- * fileModalWrapper: HTMLElement,
807
- * embedInput: HTMLInputElement,
808
- * previewSrc: HTMLElement,
809
- * proportion: HTMLInputElement,
810
- * inputX: HTMLInputElement,
811
- * inputY: HTMLInputElement,
812
- * revertBtn: HTMLButtonElement
813
- * }}
814
- */
815
- function CreateHTML_modal({ lang, icons }, pluginOptions) {
816
- let html = /*html*/ `
817
- <form method="post" enctype="multipart/form-data">
818
- <div class="se-modal-header">
819
- <button type="button" data-command="close" class="se-btn se-close-btn" title="${lang.close}" aria-label="${lang.close}">
820
- ${icons.cancel}
821
- </button>
822
- <span class="se-modal-title">${lang.embed_modal_title}</span>
823
- </div>
824
- <div class="se-modal-body">
825
- <div class='se-modal-form'>
826
- <label>${lang.embed_modal_source}</label>
827
- <input class='se-input-form se-input-url' type='text' data-focus />
828
- <pre class='se-link-preview'></pre>
829
- </div>`;
830
- if (pluginOptions.canResize) {
831
- const onlyPercentage = pluginOptions.percentageOnlySize;
832
- const onlyPercentDisplay = onlyPercentage ? ' style="display: none !important;"' : '';
833
- const heightDisplay = !pluginOptions.showHeightInput ? ' style="display: none !important;"' : '';
834
- const ratioDisplay = !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
835
- const onlyWidthDisplay = !onlyPercentage && !pluginOptions.showHeightInput && !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
836
- html += /*html*/ `
837
- <div class="se-modal-form">
838
- <div class="se-modal-size-text">
839
- <label class="size-w">${lang.width}</label>
840
- <label class="se-modal-size-x">&nbsp;</label>
841
- <label class="size-h"${heightDisplay}>${lang.height}</label>
842
- <label class="size-h"${ratioDisplay}>(${lang.ratio})</label>
843
- </div>
844
- <input class="se-input-control _se_size_x" placeholder="auto"${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}/>
845
- <label class="se-modal-size-x"${onlyWidthDisplay}>${onlyPercentage ? '%' : 'x'}</label>
846
- <input class="se-input-control _se_size_y" placeholder="auto"
847
- ${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}${heightDisplay}/>
848
- <button type="button" title="${lang.revert}" aria-label="${lang.revert}" class="se-btn se-modal-btn-revert">${icons.revert}</button>
849
- </div>
850
- <div class="se-modal-form se-modal-form-footer"${onlyPercentDisplay}${onlyWidthDisplay}>
851
- <label>
852
- <input type="checkbox" class="se-modal-btn-check _se_check_proportion" />&nbsp;
853
- <span>${lang.proportion}</span>
854
- </label>
855
- </div>`;
856
- }
857
-
858
- html += /*html*/ `
859
- </div>
860
- <div class="se-modal-footer">
861
- <div class="se-figure-align">
862
- <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="none" checked>${lang.basic}</label>
863
- <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="left">${lang.left}</label>
864
- <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="center">${lang.center}</label>
865
- <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="right">${lang.right}</label>
866
- </div>
867
- <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}"><span>${lang.submitButton}</span></button>
868
- </div>
869
- </form>`;
870
-
871
- const content = dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
872
-
873
- return {
874
- html: content,
875
- figureAlignBtn: content.querySelector('.se-figure-align'),
876
- fileModalWrapper: content.querySelector('.se-flex-input-wrapper'),
877
- embedInput: content.querySelector('.se-input-url'),
878
- previewSrc: content.querySelector('.se-link-preview'),
879
- proportion: content.querySelector('._se_check_proportion'),
880
- inputX: content.querySelector('._se_size_x'),
881
- inputY: content.querySelector('._se_size_y'),
882
- revertBtn: content.querySelector('.se-modal-btn-revert')
883
- };
884
- }
885
-
886
- export default Embed;
1
+ import EditorInjector from '../../editorInjector';
2
+ import { Modal, Figure } from '../../modules';
3
+ import { dom, numbers, env, keyCodeMap } from '../../helper';
4
+ const { NO_EVENT } = env;
5
+
6
+ /**
7
+ * @typedef {import('../../events').ProcessInfo} ProcessInfo_embed
8
+ */
9
+
10
+ /**
11
+ * @typedef {import('../../modules/Figure').FigureControls} FigureControls_embed
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} EmbedPluginOptions
16
+ * @property {boolean} [canResize=true] - Whether the embed 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 embed element (numeric value or with unit).
19
+ * @property {string} [defaultHeight] - The default height of the embed element (numeric value or with unit).
20
+ * @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing.
21
+ * @property {string} [uploadUrl] - The URL for file uploads.
22
+ * @property {Object<string, string>} [uploadHeaders] - Headers to include in file upload requests.
23
+ * @property {number} [uploadSizeLimit] - The total file upload size limit in bytes.
24
+ * @property {number} [uploadSingleSizeLimit] - The single file upload size limit in bytes.
25
+ * @property {Object<string, string>} [iframeTagAttributes] - Additional attributes to set on the iframe tag.
26
+ * @property {string} [query_youtube] - YouTube query parameter.
27
+ * @property {string} [query_vimeo] - Vimeo query parameter.
28
+ * @property {Array<RegExp>} [urlPatterns] - Additional URL patterns for embed.
29
+ * @property {Object<string, {pattern: RegExp, action: (url: string) => string, tag: string}>} [embedQuery] - Custom query objects for additional embedding services.
30
+ * Example :
31
+ * {
32
+ * facebook: {
33
+ * pattern: /(?:https?:\/\/)?(?:www\.)?(?:facebook\.com)\/(.+)/i,
34
+ * action: (url) => {
35
+ * return `https://www.facebook.com/plugins/post.php?href=${encodeURIComponent(url)}&show_text=true&width=500`;
36
+ * },
37
+ * tag: 'iframe'
38
+ * },
39
+ * twitter: {
40
+ * pattern: /(?:https?:\/\/)?(?:www\.)?(?:twitter\.com)\/(status|embed)\/(.+)/i,
41
+ * action: (url) => {
42
+ * return `https://platform.twitter.com/embed/Tweet.html?url=${encodeURIComponent(url)}`;
43
+ * },
44
+ * tag: 'iframe'
45
+ * },
46
+ * // Additional services...
47
+ * }
48
+ * @property {FigureControls_embed} [controls] - Figure controls.
49
+ */
50
+
51
+ /**
52
+ * @class
53
+ * @description Embed modal plugin.
54
+ * - This plugin provides a modal interface for embedding external content (e.g., videos, iframes) into the editor.
55
+ */
56
+ class Embed extends EditorInjector {
57
+ static key = 'embed';
58
+ static type = 'modal';
59
+ static className = '';
60
+ /**
61
+ * @this {Embed}
62
+ * @param {HTMLElement} node - The node to check.
63
+ * @returns {HTMLElement|null} Returns a node if the node is a valid component.
64
+ */
65
+ static component(node) {
66
+ let src = '';
67
+ if (dom.check.isIFrame(node)) src = node.src;
68
+ if (/^DIV$/i.test(node?.nodeName) && dom.check.isIFrame(node.firstElementChild)) src = node.firstElementChild.src;
69
+
70
+ if (src) {
71
+ return this.checkContentType(src) ? node : null;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * @constructor
78
+ * @param {__se__EditorCore} editor - The root editor instance
79
+ * @param {EmbedPluginOptions} pluginOptions
80
+ */
81
+ constructor(editor, pluginOptions) {
82
+ // plugin bisic properties
83
+ super(editor);
84
+ this.title = this.lang.embed;
85
+ this.icon = 'embed';
86
+
87
+ // define plugin options
88
+ this.pluginOptions = {
89
+ canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
90
+ showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
91
+ defaultWidth: !pluginOptions.defaultWidth || !numbers.get(pluginOptions.defaultWidth, 0) ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
92
+ defaultHeight: !pluginOptions.defaultHeight || !numbers.get(pluginOptions.defaultHeight, 0) ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
93
+ percentageOnlySize: !!pluginOptions.percentageOnlySize,
94
+ uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
95
+ uploadHeaders: pluginOptions.uploadHeaders || null,
96
+ uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
97
+ uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
98
+ iframeTagAttributes: pluginOptions.iframeTagAttributes || null,
99
+ query_youtube: pluginOptions.query_youtube || '',
100
+ query_vimeo: pluginOptions.query_vimeo || ''
101
+ };
102
+
103
+ // create HTML
104
+ const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px';
105
+ const modalEl = CreateHTML_modal(editor, this.pluginOptions);
106
+ const figureControls = pluginOptions.controls || !this.pluginOptions.canResize ? [['align', 'edit', 'copy', 'remove']] : [['resize_auto,75,50', 'align', 'edit', 'revert', 'copy', 'remove']];
107
+
108
+ // show align
109
+ if (!figureControls.some((subArray) => subArray.includes('align'))) modalEl.figureAlignBtn.style.display = 'none';
110
+
111
+ // modules
112
+ this.modal = new Modal(this, modalEl.html);
113
+ this.figure = new Figure(this, figureControls, { sizeUnit: sizeUnit });
114
+
115
+ // members
116
+ this.fileModalWrapper = modalEl.fileModalWrapper;
117
+ this.embedInput = modalEl.embedInput;
118
+ this.focusElement = this.embedInput;
119
+ this.previewSrc = modalEl.previewSrc;
120
+ this._linkValue = '';
121
+ this._align = 'none';
122
+ this._defaultSizeX = this.pluginOptions.defaultWidth;
123
+ this._defaultSizeY = this.pluginOptions.defaultHeight;
124
+ this.sizeUnit = sizeUnit;
125
+ this.proportion = null;
126
+ this.inputX = null;
127
+ this.inputY = null;
128
+ this._element = null;
129
+ this._cover = null;
130
+ this._container = null;
131
+ this._ratio = { w: 1, h: 1 };
132
+ this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
133
+ this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
134
+ this._resizing = this.pluginOptions.canResize;
135
+ this._onlyPercentage = this.pluginOptions.percentageOnlySize;
136
+ this._nonResizing = !this._resizing || !this.pluginOptions.showHeightInput || this._onlyPercentage;
137
+ this.query = {
138
+ facebook: {
139
+ pattern: /(?:https?:\/\/)?(?:www\.)?(?:facebook\.com)\/(.+)/i,
140
+ action: (url) => {
141
+ return `https://www.facebook.com/plugins/post.php?href=${encodeURIComponent(url)}&show_text=true&width=500`;
142
+ },
143
+ tag: 'iframe'
144
+ },
145
+ twitter: {
146
+ pattern: /(?:https?:\/\/)?(?:www\.)?(?:twitter\.com)\/(status|embed)\/(.+)/i,
147
+ action: (url) => {
148
+ return `https://platform.twitter.com/embed/Tweet.html?url=${encodeURIComponent(url)}`;
149
+ },
150
+ tag: 'iframe'
151
+ },
152
+ instagram: {
153
+ pattern: /(?:https?:\/\/)?(?:www\.)?(?:instagram\.com)\/p\/(.+)/i,
154
+ action: (url) => {
155
+ const postId = url.match(this.query.instagram.pattern)[1];
156
+ return `https://www.instagram.com/p/${postId}/embed`;
157
+ },
158
+ tag: 'iframe'
159
+ },
160
+ linkedin: {
161
+ pattern: /(?:https?:\/\/)?(?:www\.)?(?:linkedin\.com)\/(.+)\/(.+)/i,
162
+ action: (url) => {
163
+ return `https://www.linkedin.com/embed/feed/update/${encodeURIComponent(url.split('/').pop())}`;
164
+ },
165
+ tag: 'iframe'
166
+ },
167
+ pinterest: {
168
+ pattern: /(?:https?:\/\/)?(?:www\.)?(?:pinterest\.com)\/pin\/(.+)/i,
169
+ action: (url) => {
170
+ const pinId = url.match(this.query.pinterest.pattern)[1];
171
+ return `https://assets.pinterest.com/ext/embed.html?id=${pinId}`;
172
+ },
173
+ tag: 'iframe'
174
+ },
175
+ spotify: {
176
+ pattern: /(?:https?:\/\/)?(?:open\.)?(?:spotify\.com)\/(track|album|playlist|show|episode)\/(.+)/i,
177
+ action: (url) => {
178
+ const match = url.match(this.query.spotify.pattern);
179
+ const type = match[1];
180
+ const id = match[2];
181
+ return `https://open.spotify.com/embed/${type}/${id}`;
182
+ },
183
+ tag: 'iframe'
184
+ },
185
+ codepen: {
186
+ pattern: /(?:https?:\/\/)?(?:www\.)?(?:codepen\.io)\/(.+)\/pen\/(.+)/i,
187
+ action: (url) => {
188
+ const [, user, penId] = url.match(this.query.codepen.pattern);
189
+ return `https://codepen.io/${user}/embed/${penId}`;
190
+ },
191
+ tag: 'iframe'
192
+ },
193
+ ...pluginOptions.embedQuery
194
+ };
195
+
196
+ const urlPatterns = [];
197
+ for (const key in this.query) {
198
+ urlPatterns.push(this.query[key].pattern);
199
+ }
200
+ this.urlPatterns = urlPatterns.concat(pluginOptions.urlPatterns || []);
201
+
202
+ // init
203
+ this.eventManager.addEvent(this.embedInput, 'input', this.#OnLinkPreview.bind(this));
204
+
205
+ if (this._resizing) {
206
+ this.proportion = modalEl.proportion;
207
+ this.inputX = modalEl.inputX;
208
+ this.inputY = modalEl.inputY;
209
+ this.inputX.value = this.pluginOptions.defaultWidth;
210
+ this.inputY.value = this.pluginOptions.defaultHeight;
211
+
212
+ this.eventManager.addEvent(this.inputX, 'keyup', this.#OnInputSize.bind(this, 'x'));
213
+ this.eventManager.addEvent(this.inputY, 'keyup', this.#OnInputSize.bind(this, 'y'));
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.
221
+ */
222
+ open() {
223
+ this.modal.open();
224
+ }
225
+
226
+ /**
227
+ * @editorMethod Modules.Controller(Figure)
228
+ * @description Executes the method that is called when a target component is edited.
229
+ */
230
+ edit() {
231
+ this.modal.open();
232
+ }
233
+
234
+ /**
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)."
238
+ */
239
+ on(isUpdate) {
240
+ if (!isUpdate && this._resizing) {
241
+ this.inputX.value = this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
242
+ this.inputY.value = this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
243
+ this.proportion.disabled = true;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * @editorMethod Modules.Modal
249
+ * @description This function is called when a form within a modal window is "submit".
250
+ * @returns {Promise<boolean>} Success / failure
251
+ */
252
+ async modalAction() {
253
+ this._align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_embed_radio"]:checked')).value;
254
+
255
+ let result = false;
256
+ if (this._linkValue.length > 0) {
257
+ result = await this.submitSRC(this._linkValue);
258
+ }
259
+
260
+ if (result) this._w.setTimeout(this.component.select.bind(this.component, this._element, Embed.key), 0);
261
+
262
+ return result;
263
+ }
264
+
265
+ /**
266
+ * @editorMethod Editor.core
267
+ * @description This method is used to validate and preserve the format of the component within the editor.
268
+ * - It ensures that the structure and attributes of the element are maintained and secure.
269
+ * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
270
+ * - If the element isn't properly contained, a new container is created to retain the format.
271
+ * @returns {{query: string, method: (element: HTMLIFrameElement) => void}} The format retention object containing the query and method to process the element.
272
+ * - query: The selector query to identify the relevant elements (in this case, 'audio').
273
+ * - method:The function to execute on the element to validate and preserve its format.
274
+ * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
275
+ */
276
+ retainFormat() {
277
+ return {
278
+ query: 'iframe',
279
+ method: async (element) => {
280
+ if (!this.checkContentType(element.src)) return;
281
+
282
+ const figureInfo = Figure.GetContainer(element);
283
+ if (figureInfo && figureInfo.container && figureInfo.cover) return;
284
+
285
+ this._ready(element);
286
+ const line = this.format.getLine(element);
287
+ if (line) this._align = line.style.textAlign || line.style.float;
288
+
289
+ this._update(element);
290
+ }
291
+ };
292
+ }
293
+
294
+ /**
295
+ * @editorMethod Modules.Modal
296
+ * @description This function is called before the modal window is opened, but before it is closed.
297
+ */
298
+ init() {
299
+ Modal.OnChangeFile(this.fileModalWrapper, []);
300
+ this._linkValue = this.previewSrc.textContent = this.embedInput.value = '';
301
+
302
+ /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_embed_radio"][value="none"]')).checked = true;
303
+ this._ratio = { w: 1, h: 1 };
304
+ this._nonResizing = false;
305
+
306
+ if (this._resizing) {
307
+ this.inputX.value = this.pluginOptions.defaultWidth === this._defaultSizeX ? '' : this.pluginOptions.defaultWidth;
308
+ this.inputY.value = this.pluginOptions.defaultHeight === this._defaultSizeY ? '' : this.pluginOptions.defaultHeight;
309
+ this.proportion.checked = false;
310
+ this.proportion.disabled = true;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * @editorMethod Editor.Component
316
+ * @description Executes the method that is called when a component of a plugin is selected.
317
+ * @param {HTMLElement} target Target component element
318
+ */
319
+ select(target) {
320
+ this._ready(target);
321
+ }
322
+
323
+ /**
324
+ * @private
325
+ * @description Prepares the component for selection.
326
+ * - Ensures that the controller is properly positioned and initialized.
327
+ * - Prevents duplicate event handling if the component is already selected.
328
+ * @param {HTMLElement} target - The selected element.
329
+ */
330
+ _ready(target) {
331
+ if (!target) return;
332
+ const figureInfo = this.figure.open(target, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: false });
333
+
334
+ this._element = target;
335
+ this._cover = figureInfo.cover;
336
+ this._container = figureInfo.container;
337
+ this._caption = figureInfo.caption;
338
+ this._align = figureInfo.align;
339
+ target.style.float = '';
340
+
341
+ this._origin_w = String(figureInfo.originWidth || figureInfo.w || '');
342
+ this._origin_h = String(figureInfo.originHeight || figureInfo.h || '');
343
+
344
+ /** @type {HTMLInputElement} */
345
+ const activeAlign = this.modal.form.querySelector('input[name="suneditor_embed_radio"][value="' + this._align + '"]') || this.modal.form.querySelector('input[name="suneditor_embed_radio"][value="none"]');
346
+ activeAlign.checked = true;
347
+
348
+ if (!this._resizing) return;
349
+
350
+ const percentageRotation = this._onlyPercentage && this.figure.isVertical;
351
+ let w = percentageRotation ? '' : figureInfo.width;
352
+ if (this._onlyPercentage) {
353
+ w = numbers.get(w, 2);
354
+ if (w > 100) w = 100;
355
+ }
356
+
357
+ this.inputX.value = String(w === 'auto' ? '' : w);
358
+
359
+ if (!this._onlyPercentage) {
360
+ const h = percentageRotation ? '' : figureInfo.height;
361
+ this.inputY.value = String(h === 'auto' ? '' : h);
362
+ }
363
+
364
+ this.proportion.checked = true;
365
+ this.inputX.disabled = percentageRotation ? true : false;
366
+ this.inputY.disabled = percentageRotation ? true : false;
367
+ this.proportion.disabled = percentageRotation ? true : false;
368
+
369
+ this._ratio = this.proportion.checked
370
+ ? figureInfo.ratio
371
+ : {
372
+ w: 1,
373
+ h: 1
374
+ };
375
+ }
376
+
377
+ /**
378
+ * @editorMethod Editor.Component
379
+ * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
380
+ * @param {HTMLElement} target Target element
381
+ * @returns {Promise<void>}
382
+ */
383
+ async destroy(target) {
384
+ const targetEl = target || this._element;
385
+ const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
386
+ const focusEl = container.previousElementSibling || container.nextElementSibling;
387
+ const emptyDiv = container.parentNode;
388
+
389
+ const message = await this.triggerEvent('onEmbedDeleteBefore', { element: targetEl, container, align: this._align, url: this._linkValue });
390
+ if (message === false) return;
391
+
392
+ dom.utils.removeItem(container);
393
+ this.init();
394
+
395
+ if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
396
+ this.nodeTransform.removeAllParents(
397
+ emptyDiv,
398
+ function (current) {
399
+ return current.childNodes.length === 0;
400
+ },
401
+ null
402
+ );
403
+ }
404
+
405
+ // focus
406
+ this.editor.focusEdge(focusEl);
407
+ this.history.push(false);
408
+ }
409
+
410
+ /**
411
+ * @description Checks if the given URL matches any of the defined URL patterns.
412
+ * @param {string} url - The URL to check.
413
+ * @returns {boolean} True if the URL matches a known pattern; otherwise, false.
414
+ */
415
+ checkContentType(url) {
416
+ url = url?.toLowerCase() || '';
417
+ if (this.urlPatterns.some((pattern) => pattern.test(url))) {
418
+ return true;
419
+ }
420
+
421
+ return false;
422
+ }
423
+
424
+ /**
425
+ * @description Finds and processes the URL for embedding by matching it against known service patterns.
426
+ * @param {string} url - The original URL.
427
+ * @returns {{origin: string, url: string, tag: string}|null} An object containing the original URL, the processed URL, and the tag type (e.g., 'iframe'),
428
+ * or null if no matching pattern is found.
429
+ */
430
+ findProcessUrl(url) {
431
+ const query = this.query;
432
+ for (const key in query) {
433
+ const service = query[key];
434
+ if (service.pattern.test(url)) {
435
+ return {
436
+ origin: url,
437
+ url: service.action(url),
438
+ tag: service.tag
439
+ };
440
+ }
441
+ }
442
+
443
+ return null;
444
+ }
445
+
446
+ /**
447
+ * @description Processes the provided source (URL or embed code) and submits it for embedding.
448
+ * - It parses the input, triggers any necessary events, and creates or updates the embed component.
449
+ * @param {string} [src] - The embed source. If not provided, uses the internally stored link value.
450
+ * @returns {Promise<boolean>} A promise that resolves to true on success or false on failure.
451
+ */
452
+ async submitSRC(src) {
453
+ if (!src) src = this._linkValue;
454
+ if (!src) return false;
455
+
456
+ let embedInfo = null;
457
+ if (/^<iframe\s|^<blockquote\s/i.test(src)) {
458
+ const embedDOM = new DOMParser().parseFromString(src, 'text/html').body.children;
459
+ if (embedDOM.length === 0) return false;
460
+ embedInfo = { children: embedDOM, ...this._getInfo(), process: null };
461
+ } else {
462
+ const processUrl = this.findProcessUrl(src);
463
+ if (!processUrl) return false;
464
+ src = processUrl.url;
465
+ embedInfo = { url: src, ...this._getInfo(), process: processUrl };
466
+ }
467
+
468
+ const handler = function (infos, newInfos) {
469
+ infos = newInfos || infos;
470
+ this._create(infos.process, infos.url, infos.children, infos.inputWidth, infos.inputHeight, infos.align, infos.isUpdate);
471
+ }.bind(this, embedInfo);
472
+ // se-ts-ignore
473
+ void this._create;
474
+
475
+ const result = await this.triggerEvent('onEmbedInputBefore', {
476
+ ...embedInfo,
477
+ handler
478
+ });
479
+
480
+ if (result === undefined) return true;
481
+ if (result === false) return false;
482
+ if (result !== null && typeof result === 'object') handler(result);
483
+
484
+ if (result === true || result === NO_EVENT) handler(null);
485
+
486
+ return true;
487
+ }
488
+
489
+ /**
490
+ * @private
491
+ * @description Creates an iframe element for embedding external content.
492
+ * @returns {HTMLIFrameElement} The created iframe element.
493
+ */
494
+ _createIframeTag() {
495
+ /** @type {HTMLIFrameElement} */
496
+ const iframeTag = dom.utils.createElement('IFRAME');
497
+ this._setIframeAttrs(iframeTag);
498
+ return iframeTag;
499
+ }
500
+
501
+ /**
502
+ * @private
503
+ * @description Creates an blockquote element for embedding external content.
504
+ * @returns {HTMLElement} The created iframe element.
505
+ */
506
+ _createEmbedTag() {
507
+ const quoteTag = dom.utils.createElement('BLOCKQUOTE');
508
+ return quoteTag;
509
+ }
510
+
511
+ /**
512
+ * @private
513
+ * @description Creates an embed component (iframe or blockquote) and inserts it into the editor.
514
+ * @param {?ProcessInfo_embed} process - Processed embed information.
515
+ * @param {?string} src - The source URL.
516
+ * @param {?Node[]} children - The embed elements.
517
+ * @param {string} width - The width of the embed component.
518
+ * @param {string} height - The height of the embed component.
519
+ * @param {string} align - The alignment of the embed component.
520
+ * @param {boolean} isUpdate - Whether this is an update to an existing embed component.
521
+ */
522
+ _create(process, src, children, width, height, align, isUpdate) {
523
+ let oFrame = null;
524
+ let cover = null;
525
+ let container = null;
526
+ let scriptTag = null;
527
+
528
+ /** update */
529
+ if (isUpdate) {
530
+ oFrame = this._element;
531
+ if (oFrame.src !== src) {
532
+ const processUrl = this.findProcessUrl(src);
533
+ if (/^iframe$/i.test(processUrl?.tag) && !/^iframe$/i.test(oFrame.nodeName)) {
534
+ const newTag = this._createIframeTag();
535
+ newTag.src = src;
536
+ oFrame.replaceWith(newTag);
537
+ oFrame = newTag;
538
+ } else if (/^blockquote$/i.test(processUrl?.tag) && !/^blockquote$/i.test(oFrame.nodeName)) {
539
+ const newTag = this._createEmbedTag();
540
+ newTag.setAttribute('src', src);
541
+ oFrame.replaceWith(newTag);
542
+ oFrame = newTag;
543
+ } else {
544
+ oFrame.src = src;
545
+ }
546
+ }
547
+ container = this._container;
548
+ cover = dom.query.getParentElement(oFrame, 'FIGURE');
549
+ } else {
550
+ /** create */
551
+ if (process) {
552
+ oFrame = this._createIframeTag();
553
+ oFrame.src = src;
554
+ const figure = Figure.CreateContainer(oFrame, 'se-embed-container');
555
+ cover = figure.cover;
556
+ container = figure.container;
557
+ } else {
558
+ oFrame = children[0];
559
+ const figure = Figure.CreateContainer(oFrame, 'se-embed-container');
560
+ cover = figure.cover;
561
+ container = figure.container;
562
+
563
+ let chd = null;
564
+ let index = 0;
565
+ while ((chd = /** @type {Element} */ (children[index]))) {
566
+ if (/^script$/i.test(chd.nodeName)) {
567
+ scriptTag = dom.utils.createElement('script', { src: chd.getAttribute('src'), async: 'true' }, null);
568
+ index++;
569
+ continue;
570
+ }
571
+ cover.appendChild(chd);
572
+ }
573
+ }
574
+ }
575
+
576
+ /** rendering */
577
+ this._element = oFrame;
578
+ this._cover = cover;
579
+ this._container = container;
580
+ this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
581
+
582
+ width = width || this._defaultSizeX;
583
+ height = height || this._defaultSizeY;
584
+ const size = this.figure.getSize(oFrame);
585
+ const inputUpdate = size.w !== width || size.h !== height;
586
+ const changeSize = !isUpdate || inputUpdate;
587
+
588
+ // set size
589
+ if (changeSize) {
590
+ this._applySize(width, height);
591
+ }
592
+
593
+ // align
594
+ this.figure.setAlign(oFrame, align);
595
+
596
+ if (!isUpdate) {
597
+ this.component.insert(container, { skipCharCount: false, skipSelection: true, skipHistory: true });
598
+
599
+ if (scriptTag) {
600
+ try {
601
+ this.history.pause();
602
+
603
+ scriptTag.onload = () => {
604
+ dom.utils.removeItem(scriptTag);
605
+ scriptTag = null;
606
+ };
607
+ cover.appendChild(scriptTag);
608
+
609
+ const observer = new MutationObserver((mutations) => {
610
+ for (const mutation of mutations) {
611
+ if (mutation.type === 'childList') {
612
+ if (!oFrame.parentElement) {
613
+ this.history.resume();
614
+ this.history.push(false);
615
+ observer.disconnect();
616
+ break;
617
+ }
618
+ }
619
+ }
620
+ });
621
+
622
+ observer.observe(this.editor.frameContext.get('wysiwyg'), {
623
+ subtree: true,
624
+ childList: true
625
+ });
626
+ } catch (e) {
627
+ this.history.resume();
628
+ console.warn('[SUNEDITOR] Embed tag script load error.', e);
629
+ }
630
+ }
631
+
632
+ if (!this.options.get('componentAutoSelect')) {
633
+ const line = this.format.addLine(container, null);
634
+ if (line) this.selection.setRange(line, 0, line, 0);
635
+ }
636
+ return;
637
+ }
638
+
639
+ if (this._resizing && changeSize && this.figure.isVertical) this.figure.setTransform(oFrame, width, height, 0);
640
+ if (!scriptTag) this.history.push(false);
641
+ }
642
+
643
+ /**
644
+ * @private
645
+ * @description Updates an existing embed component within the editor.
646
+ * @param {HTMLIFrameElement} oFrame - The existing embed element to be updated.
647
+ */
648
+ _update(oFrame) {
649
+ if (!oFrame) return;
650
+
651
+ this._setIframeAttrs(oFrame);
652
+
653
+ let existElement = this.format.isBlock(oFrame.parentNode) || dom.check.isWysiwygFrame(oFrame.parentNode) ? oFrame : this.format.getLine(oFrame) || oFrame;
654
+
655
+ const prevFrame = oFrame;
656
+ oFrame = /** @type {HTMLIFrameElement} */ (oFrame.cloneNode(true));
657
+ const figure = Figure.CreateContainer(oFrame, 'se-embed-container');
658
+ const container = figure.container;
659
+
660
+ // size
661
+ this.figure.open(oFrame, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
662
+ const size = (oFrame.getAttribute('data-se-size') || ',').split(',');
663
+ this._applySize(size[0] || prevFrame.style.width || prevFrame.width || '', size[1] || prevFrame.style.height || prevFrame.height || '');
664
+
665
+ // align
666
+ const format = this.format.getLine(prevFrame);
667
+ if (format) this._align = format.style.textAlign || format.style.float;
668
+ this.figure.setAlign(oFrame, this._align);
669
+
670
+ if (dom.query.getParentElement(prevFrame, dom.check.isExcludeFormat)) {
671
+ prevFrame.replaceWith(container);
672
+ } else if (dom.check.isListCell(existElement)) {
673
+ const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
674
+ existElement.insertBefore(container, refer);
675
+ dom.utils.removeItem(prevFrame);
676
+ this.nodeTransform.removeEmptyNode(refer, null, true);
677
+ } else if (this.format.isLine(existElement)) {
678
+ const refer = dom.query.getParentElement(prevFrame, (current) => current.parentNode === existElement);
679
+ existElement = this.nodeTransform.split(existElement, refer);
680
+ existElement.parentNode.insertBefore(container, existElement);
681
+ dom.utils.removeItem(prevFrame);
682
+ this.nodeTransform.removeEmptyNode(existElement, null, true);
683
+ } else {
684
+ /** @type {Element} */ (existElement).replaceWith(container);
685
+ }
686
+
687
+ return oFrame;
688
+ }
689
+
690
+ /**
691
+ * @private
692
+ * @description Applies width and height to the embed component.
693
+ * @param {string|number} w - The width to apply.
694
+ * @param {string|number} h - The height to apply.
695
+ */
696
+ _applySize(w, h) {
697
+ if (!w) w = this.inputX?.value || this.pluginOptions.defaultWidth;
698
+ if (!h) h = this.inputY?.value || this.pluginOptions.defaultHeight;
699
+ if (this._onlyPercentage) {
700
+ if (!w) w = '100%';
701
+ else if (/%$/.test(w + '')) w += '%';
702
+ }
703
+ this.figure.setSize(w, h);
704
+ }
705
+
706
+ /**
707
+ * @private
708
+ * @description Retrieves embed component size and alignment information.
709
+ * @returns {{inputWidth: string, inputHeight: string, align: string, isUpdate: boolean, element: Element}} An object containing
710
+ * - inputWidth : The width of the embed component.
711
+ * - inputHeight : The height of the embed component.
712
+ * - align : The alignment of the embed component.
713
+ * - isUpdate : Whether the component is being updated.
714
+ * - element : The target element.
715
+ */
716
+ _getInfo() {
717
+ return {
718
+ inputWidth: this.inputX?.value || '',
719
+ inputHeight: this.inputY?.value || '',
720
+ align: this._align,
721
+ isUpdate: this.modal.isUpdate,
722
+ element: this._element
723
+ };
724
+ }
725
+
726
+ /**
727
+ * @private
728
+ * @description Sets default attributes for an iframe element.
729
+ * @param {HTMLIFrameElement} element - The iframe element to modify.
730
+ */
731
+ _setIframeAttrs(element) {
732
+ element.frameBorder = '0';
733
+ element.allowFullscreen = true;
734
+
735
+ const attrs = this.pluginOptions.iframeTagAttributes;
736
+ if (!attrs) return;
737
+
738
+ for (const key in attrs) {
739
+ element.setAttribute(key, attrs[key]);
740
+ }
741
+ }
742
+
743
+ /**
744
+ * @description Handles link preview input changes.
745
+ * @param {InputEvent} e - Event object
746
+ */
747
+ #OnLinkPreview(e) {
748
+ /** @type {HTMLInputElement} */
749
+ const eventTarget = dom.query.getEventTarget(e);
750
+ const value = eventTarget.value.trim();
751
+ if (/^<iframe.*\/iframe>$/.test(value)) {
752
+ this._linkValue = value;
753
+ this.previewSrc.textContent = '<IFrame :src=".."></IFrame>';
754
+ } else {
755
+ this._linkValue = this.previewSrc.textContent = !value
756
+ ? ''
757
+ : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
758
+ ? this.options.get('defaultUrlProtocol') + value
759
+ : !value.includes('://')
760
+ ? '/' + value
761
+ : value;
762
+ }
763
+ }
764
+
765
+ #OnClickRevert() {
766
+ if (this._onlyPercentage) {
767
+ this.inputX.value = Number(this._origin_w) > 100 ? '100' : this._origin_w;
768
+ } else {
769
+ this.inputX.value = this._origin_w;
770
+ this.inputY.value = this._origin_h;
771
+ }
772
+ }
773
+
774
+ /**
775
+ * @param {"x"|"y"} xy - x or y
776
+ * @param {KeyboardEvent} e - Event object
777
+ */
778
+ #OnInputSize(xy, e) {
779
+ if (keyCodeMap.isSpace(e.code)) {
780
+ e.preventDefault();
781
+ return;
782
+ }
783
+
784
+ /** @type {HTMLInputElement} */
785
+ const eventTarget = dom.query.getEventTarget(e);
786
+
787
+ if (xy === 'x' && this._onlyPercentage && Number(eventTarget.value) > 100) {
788
+ eventTarget.value = '100';
789
+ } else if (this.proportion.checked) {
790
+ const ratioSize = Figure.CalcRatio(this.inputX.value, this.inputY.value, this.sizeUnit, this._ratio);
791
+ if (xy === 'x') {
792
+ this.inputY.value = String(ratioSize.h);
793
+ } else {
794
+ this.inputX.value = String(ratioSize.w);
795
+ }
796
+ }
797
+ }
798
+ }
799
+
800
+ /**
801
+ * @param {__se__EditorCore} editor Editor instance
802
+ * @param {*} pluginOptions
803
+ * @returns {{
804
+ * html: HTMLElement,
805
+ * figureAlignBtn: HTMLButtonElement,
806
+ * fileModalWrapper: HTMLElement,
807
+ * embedInput: HTMLInputElement,
808
+ * previewSrc: HTMLElement,
809
+ * proportion: HTMLInputElement,
810
+ * inputX: HTMLInputElement,
811
+ * inputY: HTMLInputElement,
812
+ * revertBtn: HTMLButtonElement
813
+ * }}
814
+ */
815
+ function CreateHTML_modal({ lang, icons }, pluginOptions) {
816
+ let html = /*html*/ `
817
+ <form method="post" enctype="multipart/form-data">
818
+ <div class="se-modal-header">
819
+ <button type="button" data-command="close" class="se-btn se-close-btn" title="${lang.close}" aria-label="${lang.close}">
820
+ ${icons.cancel}
821
+ </button>
822
+ <span class="se-modal-title">${lang.embed_modal_title}</span>
823
+ </div>
824
+ <div class="se-modal-body">
825
+ <div class='se-modal-form'>
826
+ <label>${lang.embed_modal_source}</label>
827
+ <input class='se-input-form se-input-url' type='text' data-focus />
828
+ <pre class='se-link-preview'></pre>
829
+ </div>`;
830
+ if (pluginOptions.canResize) {
831
+ const onlyPercentage = pluginOptions.percentageOnlySize;
832
+ const onlyPercentDisplay = onlyPercentage ? ' style="display: none !important;"' : '';
833
+ const heightDisplay = !pluginOptions.showHeightInput ? ' style="display: none !important;"' : '';
834
+ const ratioDisplay = !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
835
+ const onlyWidthDisplay = !onlyPercentage && !pluginOptions.showHeightInput && !pluginOptions.showRatioOption ? ' style="display: none !important;"' : '';
836
+ html += /*html*/ `
837
+ <div class="se-modal-form">
838
+ <div class="se-modal-size-text">
839
+ <label class="size-w">${lang.width}</label>
840
+ <label class="se-modal-size-x">&nbsp;</label>
841
+ <label class="size-h"${heightDisplay}>${lang.height}</label>
842
+ <label class="size-h"${ratioDisplay}>(${lang.ratio})</label>
843
+ </div>
844
+ <input class="se-input-control _se_size_x" placeholder="auto"${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}/>
845
+ <label class="se-modal-size-x"${onlyWidthDisplay}>${onlyPercentage ? '%' : 'x'}</label>
846
+ <input class="se-input-control _se_size_y" placeholder="auto"
847
+ ${onlyPercentage ? ' type="number" min="1"' : 'type="text"'}${onlyPercentage ? ' max="100"' : ''}${heightDisplay}/>
848
+ <button type="button" title="${lang.revert}" aria-label="${lang.revert}" class="se-btn se-modal-btn-revert">${icons.revert}</button>
849
+ </div>
850
+ <div class="se-modal-form se-modal-form-footer"${onlyPercentDisplay}${onlyWidthDisplay}>
851
+ <label>
852
+ <input type="checkbox" class="se-modal-btn-check _se_check_proportion" />&nbsp;
853
+ <span>${lang.proportion}</span>
854
+ </label>
855
+ </div>`;
856
+ }
857
+
858
+ html += /*html*/ `
859
+ </div>
860
+ <div class="se-modal-footer">
861
+ <div class="se-figure-align">
862
+ <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="none" checked>${lang.basic}</label>
863
+ <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="left">${lang.left}</label>
864
+ <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="center">${lang.center}</label>
865
+ <label><input type="radio" name="suneditor_embed_radio" class="se-modal-btn-radio" value="right">${lang.right}</label>
866
+ </div>
867
+ <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}"><span>${lang.submitButton}</span></button>
868
+ </div>
869
+ </form>`;
870
+
871
+ const content = dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
872
+
873
+ return {
874
+ html: content,
875
+ figureAlignBtn: content.querySelector('.se-figure-align'),
876
+ fileModalWrapper: content.querySelector('.se-flex-input-wrapper'),
877
+ embedInput: content.querySelector('.se-input-url'),
878
+ previewSrc: content.querySelector('.se-link-preview'),
879
+ proportion: content.querySelector('._se_check_proportion'),
880
+ inputX: content.querySelector('._se_size_x'),
881
+ inputY: content.querySelector('._se_size_y'),
882
+ revertBtn: content.querySelector('.se-modal-btn-revert')
883
+ };
884
+ }
885
+
886
+ export default Embed;