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,1376 +1,1377 @@
1
- import EditorInjector from '../../editorInjector';
2
- import { Modal, Figure, FileManager, ModalAnchorEditor } from '../../modules';
3
- import { dom, numbers, env, keyCodeMap } from '../../helper';
4
- const { NO_EVENT } = env;
5
-
6
- /**
7
- * @typedef {import('../../events').ImageInfo} ImageInfo_image
8
- */
9
-
10
- /**
11
- * @typedef {import('../../modules/Figure').FigureControls} FigureControls_image
12
- */
13
-
14
- /**
15
- * @typedef {Object} ImagePluginOptions
16
- * @property {boolean} [canResize=true] - Whether the image element can be resized.
17
- * @property {boolean} [showHeightInput=true] - Whether to display the height input field.
18
- * @property {string} [defaultWidth="auto"] - The default width of the image. If a number is provided, "px" will be appended.
19
- * @property {string} [defaultHeight="auto"] - The default height of the image. If a number is provided, "px" will be appended.
20
- * @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing.
21
- * @property {boolean} [createFileInput=true] - Whether to create a file input element for image uploads.
22
- * @property {boolean} [createUrlInput=true] - Whether to create a URL input element for image insertion.
23
- * @property {string} [uploadUrl] - The URL endpoint for image file uploads.
24
- * @property {Object<string, string>} [uploadHeaders] - Additional headers to include in the file upload request.
25
- * @property {number} [uploadSizeLimit] - The total upload size limit in bytes.
26
- * @property {number} [uploadSingleSizeLimit] - The single file upload size limit in bytes.
27
- * @property {boolean} [allowMultiple=false] - Whether multiple image uploads are allowed.
28
- * @property {string} [acceptedFormats="image/*"] - The accepted file formats for image uploads.
29
- * @property {boolean} [useFormatType=true] - Whether to enable format type selection (block or inline).
30
- * @property {string} [defaultFormatType="block"] - The default image format type ("block" or "inline").
31
- * @property {boolean} [keepFormatType=false] - Whether to retain the chosen format type after image insertion.
32
- * @property {boolean} [linkEnableFileUpload] - Whether to enable file uploads for linked images.
33
- * @property {FigureControls_image} [controls] - Figure controls.
34
- */
35
-
36
- /**
37
- * @class
38
- * @description Image plugin.
39
- * - This plugin provides image insertion functionality within the editor, supporting both file upload and URL input.
40
- */
41
- class Image_ extends EditorInjector {
42
- static key = 'image';
43
- static type = 'modal';
44
- static className = '';
45
- /**
46
- * @this {Image_}
47
- * @param {Element} node - The node to check.
48
- * @returns {Element|null} Returns a node if the node is a valid component.
49
- */
50
- static component(node) {
51
- const compNode = dom.check.isFigure(node) || (/^span$/i.test(node.nodeName) && dom.utils.hasClass(node, 'se-component')) ? node.firstElementChild : node;
52
- return /^IMG$/i.test(compNode?.nodeName) ? compNode : dom.check.isAnchor(compNode) && /^IMG$/i.test(compNode?.firstElementChild?.nodeName) ? compNode?.firstElementChild : null;
53
- }
54
-
55
- /**
56
- * @constructor
57
- * @param {__se__EditorCore} editor - The root editor instance
58
- * @param {ImagePluginOptions} pluginOptions
59
- */
60
- constructor(editor, pluginOptions) {
61
- // plugin bisic properties
62
- super(editor);
63
- this.title = this.lang.image;
64
- this.icon = 'image';
65
-
66
- this.pluginOptions = {
67
- canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
68
- showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
69
- defaultWidth: !pluginOptions.defaultWidth ? 'auto' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
70
- defaultHeight: !pluginOptions.defaultHeight ? 'auto' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
71
- percentageOnlySize: !!pluginOptions.percentageOnlySize,
72
- createFileInput: pluginOptions.createFileInput === undefined ? true : pluginOptions.createFileInput,
73
- createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
74
- uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
75
- uploadHeaders: pluginOptions.uploadHeaders || null,
76
- uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
77
- uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
78
- allowMultiple: !!pluginOptions.allowMultiple,
79
- acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'image/*' : pluginOptions.acceptedFormats.trim() || 'image/*',
80
- useFormatType: pluginOptions.useFormatType ?? true,
81
- defaultFormatType: ['block', 'inline'].includes(pluginOptions.defaultFormatType) ? pluginOptions.defaultFormatType : 'block',
82
- keepFormatType: pluginOptions.keepFormatType ?? false
83
- };
84
-
85
- // create HTML
86
- const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px';
87
- const modalEl = CreateHTML_modal(editor, this.pluginOptions);
88
- const ctrlAs = this.pluginOptions.useFormatType ? 'as' : '';
89
- const figureControls =
90
- pluginOptions.controls || !this.pluginOptions.canResize
91
- ? [[ctrlAs, 'mirror_h', 'mirror_v', 'align', 'caption', 'edit', 'revert', 'copy', 'remove']]
92
- : [
93
- [ctrlAs, 'resize_auto,100,75,50', 'rotate_l', 'rotate_r', 'mirror_h', 'mirror_v'],
94
- ['align', 'caption', 'edit', 'revert', 'copy', 'remove']
95
- ];
96
-
97
- // show align
98
- this.alignForm = modalEl.alignForm;
99
- if (!figureControls.some((subArray) => subArray.includes('align'))) this.alignForm.style.display = 'none';
100
-
101
- // modules
102
- const Link = this.plugins.link ? this.plugins.link.pluginOptions : {};
103
- this.anchor = new ModalAnchorEditor(this, modalEl.html, {
104
- textToDisplay: false,
105
- title: true,
106
- openNewWindow: Link.openNewWindow,
107
- relList: Link.relList,
108
- defaultRel: Link.defaultRel,
109
- noAutoPrefix: Link.noAutoPrefix,
110
- enableFileUpload: pluginOptions.linkEnableFileUpload
111
- });
112
- this.modal = new Modal(this, modalEl.html);
113
- this.figure = new Figure(this, figureControls, {
114
- sizeUnit: sizeUnit
115
- });
116
- this.fileManager = new FileManager(this, {
117
- query: 'img',
118
- loadHandler: this.events.onImageLoad,
119
- eventHandler: this.events.onImageAction
120
- });
121
-
122
- // members
123
- this.fileModalWrapper = modalEl.fileModalWrapper;
124
- this.imgInputFile = modalEl.imgInputFile;
125
- this.imgUrlFile = modalEl.imgUrlFile;
126
- this.focusElement = this.imgInputFile || this.imgUrlFile;
127
- this.altText = modalEl.altText;
128
- this.captionCheckEl = modalEl.captionCheckEl;
129
- this.captionEl = this.captionCheckEl?.parentElement;
130
- this.previewSrc = modalEl.previewSrc;
131
- this.sizeUnit = sizeUnit;
132
- this.as = 'block';
133
- this.proportion = null;
134
- this.inputX = null;
135
- this.inputY = null;
136
- this._linkElement = null;
137
- this._linkValue = '';
138
- this._align = 'none';
139
- this._svgDefaultSize = '30%';
140
- this._base64RenderIndex = 0;
141
- this._element = null;
142
- this._cover = null;
143
- this._container = null;
144
- this._caption = null;
145
- this._ratio = {
146
- w: 1,
147
- h: 1
148
- };
149
- this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
150
- this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
151
- this._resizing = this.pluginOptions.canResize;
152
- this._onlyPercentage = this.pluginOptions.percentageOnlySize;
153
- this._nonResizing = !this._resizing || !this.pluginOptions.showHeightInput || this._onlyPercentage;
154
-
155
- // init
156
- this.eventManager.addEvent(modalEl.tabs, 'click', this.#OpenTab.bind(this));
157
- if (this.imgInputFile) this.eventManager.addEvent(modalEl.fileRemoveBtn, 'click', this.#RemoveSelectedFiles.bind(this));
158
- if (this.imgUrlFile) this.eventManager.addEvent(this.imgUrlFile, 'input', this.#OnLinkPreview.bind(this));
159
- if (this.imgInputFile && this.imgUrlFile) this.eventManager.addEvent(this.imgInputFile, 'change', this.#OnfileInputChange.bind(this));
160
-
161
- const galleryButton = modalEl.galleryButton;
162
- if (galleryButton) this.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this));
163
-
164
- if (this._resizing) {
165
- this.proportion = modalEl.proportion;
166
- this.inputX = modalEl.inputX;
167
- this.inputY = modalEl.inputY;
168
- this.inputX.value = this.pluginOptions.defaultWidth;
169
- this.inputY.value = this.pluginOptions.defaultHeight;
170
-
171
- const ratioChange = this.#OnChangeRatio.bind(this);
172
- this.eventManager.addEvent(this.inputX, 'keyup', this.#OnInputSize.bind(this, 'x'));
173
- this.eventManager.addEvent(this.inputY, 'keyup', this.#OnInputSize.bind(this, 'y'));
174
- this.eventManager.addEvent(this.inputX, 'change', ratioChange);
175
- this.eventManager.addEvent(this.inputY, 'change', ratioChange);
176
- this.eventManager.addEvent(this.proportion, 'change', ratioChange);
177
- this.eventManager.addEvent(modalEl.revertBtn, 'click', this.#OnClickRevert.bind(this));
178
- }
179
-
180
- if (this.pluginOptions.useFormatType) {
181
- this.as = this.pluginOptions.defaultFormatType;
182
- this.asBlock = modalEl.asBlock;
183
- this.asInline = modalEl.asInline;
184
- this.eventManager.addEvent([this.asBlock, this.asInline], 'click', this.#OnClickAsButton.bind(this));
185
- }
186
- }
187
-
188
- /**
189
- * @editorMethod Modules.Modal
190
- * @description Executes the method that is called when a "Modal" module's is opened.
191
- */
192
- open() {
193
- this.modal.open();
194
- }
195
-
196
- /**
197
- * @editorMethod Modules.Controller(Figure)
198
- * @description Executes the method that is called when a target component is edited.
199
- */
200
- edit() {
201
- this.modal.open();
202
- }
203
-
204
- /**
205
- * @editorMethod Modules.Modal
206
- * @description Executes the method that is called when a plugin's modal is opened.
207
- * @param {boolean} isUpdate "Indicates whether the modal is for editing an existing component (true) or registering a new one (false)."
208
- */
209
- on(isUpdate) {
210
- if (!isUpdate) {
211
- if (this._resizing) {
212
- this.inputX.value = this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
213
- this.inputY.value = this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
214
- }
215
- if (this.imgInputFile && this.pluginOptions.allowMultiple) this.imgInputFile.setAttribute('multiple', 'multiple');
216
- } else {
217
- if (this.imgInputFile && this.pluginOptions.allowMultiple) this.imgInputFile.removeAttribute('multiple');
218
- }
219
-
220
- this.anchor.on(isUpdate);
221
- }
222
-
223
- /**
224
- * @editorMethod Editor.EventManager
225
- * @description Executes the event function of "paste" or "drop".
226
- * @param {Object} params { frameContext, event, file }
227
- * @param {__se__FrameContext} params.frameContext Frame context
228
- * @param {ClipboardEvent} params.event Event object
229
- * @param {File} params.file File object
230
- * @returns {boolean} - If return false, the file upload will be canceled
231
- */
232
- onFilePasteAndDrop({ file }) {
233
- if (!/^image/.test(file.type)) return;
234
-
235
- this.submitFile([file]);
236
- this.editor.focus();
237
-
238
- return false;
239
- }
240
-
241
- /**
242
- * @editorMethod Modules.Modal
243
- * @description This function is called when a form within a modal window is "submit".
244
- * @returns {Promise<boolean>} Success or failure
245
- */
246
- async modalAction() {
247
- this._align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_image_radio"]:checked')).value;
248
-
249
- if (this.modal.isUpdate) {
250
- this._update(this.inputX?.value, this.inputY?.value);
251
- this.history.push(false);
252
- }
253
-
254
- if (this.imgInputFile && this.imgInputFile.files.length > 0) {
255
- return await this.submitFile(this.imgInputFile.files);
256
- } else if (this.imgUrlFile && this._linkValue.length > 0) {
257
- return await this.submitURL(this._linkValue);
258
- }
259
-
260
- return false;
261
- }
262
-
263
- /**
264
- * @editorMethod Editor.core
265
- * @description This method is used to validate and preserve the format of the component within the editor.
266
- * - It ensures that the structure and attributes of the element are maintained and secure.
267
- * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
268
- * - If the element isn't properly contained, a new container is created to retain the format.
269
- * @returns {{query: string, method: (element: HTMLImageElement) => void}} The format retention object containing the query and method to process the element.
270
- * - query: The selector query to identify the relevant elements (in this case, 'audio').
271
- * - method:The function to execute on the element to validate and preserve its format.
272
- * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
273
- */
274
- retainFormat() {
275
- return {
276
- query: 'img',
277
- method: (element) => {
278
- const figureInfo = Figure.GetContainer(element);
279
- if (figureInfo && figureInfo.container && figureInfo.cover) return;
280
-
281
- this._ready(element);
282
- this._fileCheck(this._origin_w, this._origin_h);
283
- }
284
- };
285
- }
286
-
287
- /**
288
- * @editorMethod Modules.Modal
289
- * @description This function is called before the modal window is opened, but before it is closed.
290
- */
291
- init() {
292
- Modal.OnChangeFile(this.fileModalWrapper, []);
293
- if (this.imgInputFile) this.imgInputFile.value = '';
294
- if (this.imgUrlFile) this._linkValue = this.previewSrc.textContent = this.imgUrlFile.value = '';
295
- if (this.imgInputFile && this.imgUrlFile) {
296
- this.imgUrlFile.disabled = false;
297
- this.previewSrc.style.textDecoration = '';
298
- }
299
-
300
- this.altText.value = '';
301
- /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_image_radio"][value="none"]')).checked = true;
302
- this.captionCheckEl.checked = false;
303
- this._element = null;
304
- this._ratio = {
305
- w: 1,
306
- h: 1
307
- };
308
- this.#OpenTab('init');
309
-
310
- if (this._resizing) {
311
- this.inputX.value = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
312
- this.inputY.value = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
313
- this.proportion.checked = true;
314
- }
315
-
316
- if (this.pluginOptions.useFormatType) {
317
- this._activeAsInline((this.pluginOptions.keepFormatType ? this.as : this.pluginOptions.defaultFormatType) === 'inline');
318
- }
319
-
320
- this.anchor.init();
321
- }
322
-
323
- /**
324
- * @editorMethod Editor.Component
325
- * @description Executes the method that is called when a component of a plugin is selected.
326
- * @param {HTMLElement} target Target component element
327
- */
328
- select(target) {
329
- this._ready(target);
330
- }
331
-
332
- /**
333
- * @private
334
- * @description Prepares the component for selection.
335
- * - Ensures that the controller is properly positioned and initialized.
336
- * - Prevents duplicate event handling if the component is already selected.
337
- * @param {HTMLElement} target - The selected element.
338
- */
339
- _ready(target) {
340
- if (!target) return;
341
- const figureInfo = this.figure.open(target, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: false });
342
- this.anchor.set(dom.check.isAnchor(target.parentNode) ? target.parentNode : null);
343
-
344
- this._linkElement = this.anchor.currentTarget;
345
- this._element = target;
346
- this._cover = figureInfo.cover;
347
- this._container = figureInfo.container;
348
- this._caption = figureInfo.caption;
349
- this._align = figureInfo.align;
350
- target.style.float = '';
351
-
352
- this._origin_w = String(figureInfo.originWidth || figureInfo.w || '');
353
- this._origin_h = String(figureInfo.originHeight || figureInfo.h || '');
354
- this.altText.value = this._element.alt;
355
-
356
- if (this.imgUrlFile) this._linkValue = this.previewSrc.textContent = this.imgUrlFile.value = this._element.src;
357
-
358
- /** @type {HTMLInputElement} */
359
- const activeAlign = this.modal.form.querySelector('input[name="suneditor_image_radio"][value="' + this._align + '"]') || this.modal.form.querySelector('input[name="suneditor_image_radio"][value="none"]');
360
- activeAlign.checked = true;
361
- this.captionCheckEl.checked = !!this._caption;
362
-
363
- if (!this._resizing) return;
364
-
365
- const percentageRotation = this._onlyPercentage && this.figure.isVertical;
366
- let w = percentageRotation ? '' : figureInfo.width;
367
- if (this._onlyPercentage) {
368
- w = numbers.get(w, 2);
369
- if (w > 100) w = 100;
370
- }
371
- this.inputX.value = String(w === 'auto' ? '' : w);
372
-
373
- if (!this._onlyPercentage) {
374
- const h = percentageRotation ? '' : figureInfo.height;
375
- this.inputY.value = String(h === 'auto' ? '' : h);
376
- }
377
-
378
- this.proportion.checked = true;
379
- this.inputX.disabled = percentageRotation ? true : false;
380
- this.inputY.disabled = percentageRotation ? true : false;
381
- this.proportion.disabled = percentageRotation ? true : false;
382
-
383
- this._ratio = this.proportion.checked
384
- ? figureInfo.ratio
385
- : {
386
- w: 1,
387
- h: 1
388
- };
389
-
390
- if (this.pluginOptions.useFormatType) {
391
- this._activeAsInline(this.component.isInline(figureInfo.container));
392
- }
393
- }
394
-
395
- /**
396
- * @editorMethod Editor.Component
397
- * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
398
- * @param {HTMLElement} target Target element
399
- * @returns {Promise<void>}
400
- */
401
- async destroy(target) {
402
- const targetEl = target || this._element;
403
- const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
404
- const focusEl = container.previousElementSibling || container.nextElementSibling;
405
- const emptyDiv = container.parentNode;
406
-
407
- const message = await this.triggerEvent('onImageDeleteBefore', { element: targetEl, container, align: this._align, alt: this.altText.value, url: this._linkValue });
408
- if (message === false) return;
409
-
410
- dom.utils.removeItem(container);
411
- this.init();
412
-
413
- if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
414
- this.nodeTransform.removeAllParents(
415
- emptyDiv,
416
- function (current) {
417
- return current.childNodes.length === 0;
418
- },
419
- null
420
- );
421
- }
422
-
423
- // focus
424
- this.editor.focusEdge(focusEl);
425
- this.history.push(false);
426
- }
427
-
428
- /**
429
- * @private
430
- * @description Retrieves the current image information.
431
- * @returns {*} - The image data.
432
- */
433
- _getInfo() {
434
- return {
435
- element: this._element,
436
- anchor: this.anchor.create(true),
437
- inputWidth: this.inputX?.value || '',
438
- inputHeight: this.inputY?.value || '',
439
- align: this._align,
440
- isUpdate: this.modal.isUpdate,
441
- alt: this.altText.value
442
- };
443
- }
444
-
445
- /**
446
- * @private
447
- * @description Toggles between block and inline image format.
448
- * @param {boolean} isInline - Whether the image should be inline.
449
- */
450
- _activeAsInline(isInline) {
451
- if (isInline) {
452
- dom.utils.addClass(this.asInline, 'on');
453
- dom.utils.removeClass(this.asBlock, 'on');
454
- this.as = 'inline';
455
- // buttns
456
- if (this.alignForm) this.alignForm.style.display = 'none';
457
- // caption
458
- if (this.captionEl) this.captionEl.style.display = 'none';
459
- } else {
460
- dom.utils.addClass(this.asBlock, 'on');
461
- dom.utils.removeClass(this.asInline, 'on');
462
- this.as = 'block';
463
- // buttns
464
- if (this.alignForm) this.alignForm.style.display = '';
465
- // caption
466
- if (this.captionEl) this.captionEl.style.display = '';
467
- }
468
- }
469
-
470
- /**
471
- * @description Create an "image" component using the provided files.
472
- * @param {FileList|File[]} fileList File object list
473
- * @returns {Promise<boolean>} If return false, the file upload will be canceled
474
- */
475
- async submitFile(fileList) {
476
- if (fileList.length === 0) return false;
477
-
478
- let fileSize = 0;
479
- const files = [];
480
- const slngleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
481
- for (let i = 0, len = fileList.length, f, s; i < len; i++) {
482
- f = fileList[i];
483
- if (!/image/i.test(f.type)) continue;
484
-
485
- s = f.size;
486
- if (slngleSizeLimit && slngleSizeLimit > s) {
487
- const err = '[SUNEDITOR.imageUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
488
- const message = await this.triggerEvent('onImageUploadError', {
489
- error: err,
490
- limitSize: slngleSizeLimit,
491
- uploadSize: s,
492
- file: f
493
- });
494
-
495
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
496
-
497
- return false;
498
- }
499
-
500
- files.push(f);
501
- fileSize += s;
502
- }
503
-
504
- const limitSize = this.pluginOptions.uploadSizeLimit;
505
- const currentSize = this.fileManager.getSize();
506
- if (limitSize > 0 && fileSize + currentSize > limitSize) {
507
- const err = '[SUNEDITOR.imageUpload.fail] Size of uploadable total images: ' + limitSize / 1000 + 'KB';
508
- const message = await this.triggerEvent('onImageUploadError', {
509
- error: err,
510
- limitSize,
511
- currentSize,
512
- uploadSize: fileSize
513
- });
514
-
515
- this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
516
-
517
- return false;
518
- }
519
-
520
- const imgInfo = { files, ...this._getInfo() };
521
- const handler = function (infos, newInfos) {
522
- infos = newInfos || infos;
523
- this._serverUpload(infos, infos.files);
524
- }.bind(this, imgInfo);
525
- // se-ts-ignore
526
- this._serverUpload;
527
-
528
- const result = await this.triggerEvent('onImageUploadBefore', {
529
- info: imgInfo,
530
- handler
531
- });
532
-
533
- if (result === undefined) return true;
534
- if (result === false) return false;
535
- if (result !== null && typeof result === 'object') handler(result);
536
-
537
- if (result === true || result === NO_EVENT) handler(null);
538
- }
539
-
540
- /**
541
- * @description Create an "image" component using the provided url.
542
- * @param {string} url File url
543
- * @returns {Promise<boolean>} If return false, the file upload will be canceled
544
- */
545
- async submitURL(url) {
546
- if (!url) url = this._linkValue;
547
- if (!url) return false;
548
-
549
- const file = { name: url.split('/').pop(), size: 0 };
550
- const imgInfo = {
551
- url,
552
- files: file,
553
- ...this._getInfo()
554
- };
555
-
556
- const handler = function (infos, newInfos) {
557
- infos = newInfos || infos;
558
- const infoUrl = infos.url;
559
- if (this.modal.isUpdate) this._updateSrc(infoUrl, infos.element, infos.files);
560
- else this._produce(infoUrl, infos.anchor, infos.inputWidth, infos.inputHeight, infos.align, infos.files, infos.alt);
561
- }.bind(this, imgInfo);
562
-
563
- const result = await this.triggerEvent('onImageUploadBefore', {
564
- info: imgInfo,
565
- handler
566
- });
567
-
568
- if (result === undefined) return true;
569
- if (result === false) return false;
570
- if (result !== null && typeof result === 'object') handler(result);
571
-
572
- if (result === true || result === NO_EVENT) handler(null);
573
-
574
- return true;
575
- }
576
-
577
- /**
578
- * @private
579
- * @description Updates the selected image size, alt text, and caption.
580
- * @param {string} width - New image width.
581
- * @param {string} height - New image height.
582
- */
583
- _update(width, height) {
584
- if (!width) width = this.inputX?.value || 'auto';
585
- if (!height) height = this.inputY?.value || 'auto';
586
-
587
- let imageEl = this._element;
588
- const cover = this._cover;
589
- const container = this._container === this._cover ? null : this._container;
590
-
591
- // check size
592
- let changeSize;
593
- const x = numbers.is(width) ? width + this.sizeUnit : width;
594
- const y = numbers.is(height) ? height + this.sizeUnit : height;
595
- if (/%$/.test(imageEl.style.width)) {
596
- changeSize = x !== container.style.width || y !== container.style.height;
597
- } else {
598
- changeSize = x !== imageEl.style.width || y !== imageEl.style.height;
599
- }
600
-
601
- // alt
602
- imageEl.alt = this.altText.value;
603
-
604
- // caption
605
- let modifiedCaption = false;
606
- if (this.captionCheckEl.checked) {
607
- if (!this._caption) {
608
- this._caption = Figure.CreateCaption(cover, this.lang.caption);
609
- modifiedCaption = true;
610
- }
611
- } else {
612
- if (this._caption) {
613
- dom.utils.removeItem(this._caption);
614
- this._caption = null;
615
- modifiedCaption = true;
616
- }
617
- }
618
-
619
- // link
620
- let isNewAnchor = false;
621
- const anchor = this.anchor.create(true);
622
- if (anchor) {
623
- if (this._linkElement !== anchor || !container.contains(anchor)) {
624
- this._linkElement = anchor.cloneNode(false);
625
- cover.insertBefore(this._setAnchor(imageEl, this._linkElement), this._caption);
626
- isNewAnchor = true;
627
- }
628
- } else if (this._linkElement !== null) {
629
- if (cover.contains(this._linkElement)) {
630
- const newEl = imageEl.cloneNode(true);
631
- cover.removeChild(this._linkElement);
632
- cover.insertBefore(newEl, this._caption);
633
- imageEl = newEl;
634
- }
635
- }
636
-
637
- // size
638
- if (this._resizing && changeSize) {
639
- this._applySize(width, height);
640
- }
641
-
642
- if (isNewAnchor) {
643
- dom.utils.removeItem(anchor);
644
- }
645
-
646
- // transform
647
- if (modifiedCaption || (!this._onlyPercentage && changeSize)) {
648
- if (/\d+/.test(imageEl.style.height) || (this.figure.isVertical && this.captionCheckEl.checked)) {
649
- if (/auto|%$/.test(width) || /auto|%$/.test(height)) {
650
- this.figure.deleteTransform(imageEl);
651
- } else {
652
- this.figure.setTransform(imageEl, width, height, 0);
653
- }
654
- }
655
- }
656
-
657
- // align
658
- this.figure.setAlign(imageEl, this._align);
659
-
660
- // select
661
- imageEl.onload = () => {
662
- this.select(imageEl);
663
- };
664
- }
665
-
666
- /**
667
- * @private
668
- * @description Validates the image size and applies necessary transformations.
669
- * @param {string} width - The width of the image.
670
- * @param {string} height - The height of the image.
671
- */
672
- _fileCheck(width, height) {
673
- if (!width) width = this.inputX?.value || 'auto';
674
- if (!height) height = this.inputY?.value || 'auto';
675
-
676
- let imageEl = this._element;
677
- let cover = this._cover;
678
- let inlineCover = null;
679
- let container = this._container === this._cover ? null : this._container;
680
- let isNewContainer = false;
681
-
682
- if (!cover || !container) {
683
- isNewContainer = true;
684
- imageEl = this._element.cloneNode(true);
685
- const figureInfo =
686
- this.pluginOptions.useFormatType && width !== 'auto' && (/^span$/i.test(this._element.parentElement?.nodeName) || this.format.isLine(this._element.parentElement))
687
- ? Figure.CreateInlineContainer(imageEl, 'se-image-container')
688
- : Figure.CreateContainer(imageEl, 'se-image-container');
689
- cover = figureInfo.cover;
690
- container = figureInfo.container;
691
- inlineCover = figureInfo.inlineCover;
692
- this.figure.open(imageEl, { nonResizing: true, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
693
- }
694
-
695
- // check size
696
- let changeSize;
697
- const x = numbers.is(width) ? width + this.sizeUnit : width;
698
- const y = numbers.is(height) ? height + this.sizeUnit : height;
699
- if (/%$/.test(imageEl.style.width)) {
700
- changeSize = x !== container.style.width || y !== container.style.height;
701
- } else {
702
- changeSize = x !== imageEl.style.width || y !== imageEl.style.height;
703
- }
704
-
705
- // alt
706
- imageEl.alt = this.altText.value;
707
-
708
- // caption
709
- let modifiedCaption = false;
710
- if (!inlineCover) {
711
- if (this.captionCheckEl.checked) {
712
- if (!this._caption || isNewContainer) {
713
- this._caption = Figure.CreateCaption(cover, this.lang.caption);
714
- modifiedCaption = true;
715
- }
716
- } else {
717
- if (this._caption) {
718
- dom.utils.removeItem(this._caption);
719
- this._caption = null;
720
- modifiedCaption = true;
721
- }
722
- }
723
- }
724
-
725
- // link
726
- let isNewAnchor = null;
727
- const anchor = this.anchor.create(true);
728
- if (anchor) {
729
- if (this._linkElement !== anchor || (isNewContainer && !container.contains(anchor))) {
730
- this._linkElement = anchor.cloneNode(false);
731
- cover.insertBefore(this._setAnchor(imageEl, this._linkElement), this._caption);
732
- isNewAnchor = this._element;
733
- }
734
- } else if (this._linkElement !== null) {
735
- if (cover.contains(this._linkElement)) {
736
- const newEl = imageEl.cloneNode(true);
737
- cover.removeChild(this._linkElement);
738
- cover.insertBefore(newEl, this._caption);
739
- imageEl = newEl;
740
- }
741
- }
742
-
743
- if (isNewContainer) {
744
- imageEl = this._element;
745
- this.figure.retainFigureFormat(container, this._element, isNewAnchor ? anchor : null, this.fileManager);
746
- this._element = imageEl = container.querySelector('img');
747
- this._cover = cover;
748
- this._container = container;
749
- }
750
-
751
- // size
752
- if (this._resizing && changeSize) {
753
- this._applySize(width, height);
754
- }
755
-
756
- if (isNewAnchor) {
757
- if (!isNewContainer) {
758
- dom.utils.removeItem(anchor);
759
- } else {
760
- dom.utils.removeItem(isNewAnchor);
761
- if (dom.query.getListChildren(anchor, (current) => /IMG/i.test(current.tagName)).length === 0) {
762
- dom.utils.removeItem(anchor);
763
- }
764
- }
765
- }
766
-
767
- // transform
768
- if (modifiedCaption || (!this._onlyPercentage && changeSize)) {
769
- if (/\d+/.test(imageEl.style.height) || (this.figure.isVertical && this.captionCheckEl.checked)) {
770
- if (/auto|%$/.test(width) || /auto|%$/.test(height)) {
771
- this.figure.deleteTransform(imageEl);
772
- } else {
773
- this.figure.setTransform(imageEl, width, height, 0);
774
- }
775
- }
776
- }
777
-
778
- // align
779
- this.figure.setAlign(imageEl, this._align);
780
- }
781
-
782
- /**
783
- * @description Opens a specific tab inside the modal.
784
- * @param {MouseEvent|string} e - The event object or tab name.
785
- * @returns {boolean} - Whether the tab was successfully opened.
786
- */
787
- #OpenTab(e) {
788
- const modalForm = this.modal.form;
789
- const targetElement = typeof e === 'string' ? modalForm.querySelector('._se_tab_link') : dom.query.getEventTarget(e);
790
-
791
- if (!/^BUTTON$/i.test(targetElement.tagName)) {
792
- return false;
793
- }
794
-
795
- // Declare all variables
796
- const tabName = targetElement.getAttribute('data-tab-link');
797
- let i;
798
-
799
- // Get all elements with class="tabcontent" and hide them
800
- const tabContent = /** @type {HTMLCollectionOf<HTMLElement>}*/ (modalForm.getElementsByClassName('_se_tab_content'));
801
- for (i = 0; i < tabContent.length; i++) {
802
- tabContent[i].style.display = 'none';
803
- }
804
-
805
- // Get all elements with class="tablinks" and remove the class "active"
806
- const tabLinks = modalForm.getElementsByClassName('_se_tab_link');
807
- for (i = 0; i < tabLinks.length; i++) {
808
- dom.utils.removeClass(tabLinks[i], 'active');
809
- }
810
-
811
- // Show the current tab, and add an "active" class to the button that opened the tab
812
- /** @type {HTMLElement}*/ (modalForm.querySelector('._se_tab_content_' + tabName)).style.display = 'block';
813
- dom.utils.addClass(targetElement, 'active');
814
-
815
- // focus
816
- if (e !== 'init') {
817
- if (tabName === 'image') {
818
- this.focusElement.focus();
819
- } else if (tabName === 'url') {
820
- this.anchor.urlInput.focus();
821
- }
822
- }
823
-
824
- return false;
825
- }
826
-
827
- /**
828
- * @private
829
- * @description Creates a new image component based on provided parameters.
830
- * @param {string} src - The image source URL.
831
- * @param {?Node} anchor - Optional anchor wrapping the image.
832
- * @param {string} width - Image width.
833
- * @param {string} height - Image height.
834
- * @param {string} align - Image alignment.
835
- * @param {{name: string, size: number}} file - File metadata.
836
- * @param {string} alt - Alternative text.
837
- */
838
- _produce(src, anchor, width, height, align, file, alt) {
839
- if (this.as !== 'inline') {
840
- this.create(src, anchor, width, height, align, file, alt);
841
- } else {
842
- this.createInline(src, anchor, width, height, file, alt);
843
- }
844
- }
845
-
846
- /**
847
- * @private
848
- * @description Applies the specified width and height to the image.
849
- * @param {string} w - Image width.
850
- * @param {string} h - Image height.
851
- */
852
- _applySize(w, h) {
853
- if (!w) w = this.inputX?.value || this.pluginOptions.defaultWidth;
854
- if (!h) h = this.inputY?.value || this.pluginOptions.defaultHeight;
855
- if (this._onlyPercentage) {
856
- if (!w) w = '100%';
857
- else if (/%$/.test(w)) w += '%';
858
- }
859
- this.figure.setSize(w, h);
860
- }
861
-
862
- /**
863
- * @description Creates a new image component, wraps it in a figure container with an optional anchor,
864
- * - applies size and alignment settings, and inserts it into the editor.
865
- * @param {string} src - The URL of the image to be inserted.
866
- * @param {?Node} anchor - An optional anchor element to wrap the image. If provided, a clone is used.
867
- * @param {string} width - The width value to be applied to the image.
868
- * @param {string} height - The height value to be applied to the image.
869
- * @param {string} align - The alignment setting for the image (e.g., 'left', 'center', 'right').
870
- * @param {{name: string, size: number}} file - File metadata associated with the image
871
- * @param {string} alt - The alternative text for the image.
872
- */
873
- create(src, anchor, width, height, align, file, alt) {
874
- /** @type {HTMLImageElement} */
875
- const oImg = dom.utils.createElement('IMG');
876
- oImg.src = src;
877
- oImg.alt = alt;
878
- anchor = this._setAnchor(oImg, anchor ? anchor.cloneNode(false) : null);
879
-
880
- const figureInfo = Figure.CreateContainer(anchor, 'se-image-container');
881
- const cover = figureInfo.cover;
882
- const container = figureInfo.container;
883
-
884
- // caption
885
- if (this.captionCheckEl.checked) {
886
- this._caption = Figure.CreateCaption(cover, this.lang.caption);
887
- }
888
-
889
- this._element = oImg;
890
- this._cover = cover;
891
- this._container = container;
892
- this.figure.open(oImg, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
893
-
894
- // set size
895
- this._applySize(width, height);
896
-
897
- // align
898
- this.figure.setAlign(oImg, align);
899
-
900
- this.fileManager.setFileData(oImg, file);
901
-
902
- oImg.onload = this.#OnloadImg.bind(this, oImg, this._svgDefaultSize, container);
903
- this.component.insert(container, { skipCharCount: false, skipSelection: !this.options.get('componentAutoSelect'), skipHistory: false });
904
- }
905
-
906
- /**
907
- * @description Creates a new inline image component, wraps it in an inline figure container with an optional anchor,
908
- * - applies size settings, and inserts it into the editor.
909
- * @param {string} src - The URL of the image to be inserted.
910
- * @param {?Node} anchor - An optional anchor element to wrap the image. If provided, a clone is used.
911
- * @param {string} width - The width value to be applied to the image.
912
- * @param {string} height - The height value to be applied to the image.
913
- * @param {{name: string, size: number}} file - File metadata associated with the image
914
- * @param {string} alt - The alternative text for the image.
915
- */
916
- createInline(src, anchor, width, height, file, alt) {
917
- /** @type {HTMLImageElement} */
918
- const oImg = dom.utils.createElement('IMG');
919
- oImg.src = src;
920
- oImg.alt = alt;
921
- anchor = this._setAnchor(oImg, anchor ? anchor.cloneNode(false) : null);
922
-
923
- const figureInfo = Figure.CreateInlineContainer(anchor, 'se-image-container');
924
- const container = figureInfo.container;
925
-
926
- this._element = oImg;
927
- this._container = container;
928
- this.figure.open(oImg, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
929
-
930
- // set size
931
- this._applySize(width, height);
932
-
933
- this.fileManager.setFileData(oImg, file);
934
-
935
- oImg.onload = this.#OnloadImg.bind(this, oImg, this._svgDefaultSize, container);
936
- this.component.insert(container, { skipCharCount: false, skipSelection: true, skipHistory: false });
937
- }
938
-
939
- /**
940
- * @private
941
- * @description Updates the image source URL.
942
- * @param {string} src - The new image source.
943
- * @param {HTMLImageElement} element - The image element.
944
- * @param {{ name: string, size: number }} file - File metadata.
945
- */
946
- _updateSrc(src, element, file) {
947
- element.src = src;
948
- this.fileManager.setFileData(element, file);
949
- this.component.select(element, Image_.key);
950
- }
951
-
952
- /**
953
- * @private
954
- * @description Registers the uploaded image and inserts it into the editor.
955
- * @param {ImageInfo_image} info - Image info.
956
- * @param {Object<string, *>} response - Server response data.
957
- */
958
- _register(info, response) {
959
- const fileList = response.result;
960
-
961
- for (let i = 0, len = fileList.length, file; i < len; i++) {
962
- file = {
963
- name: fileList[i].name,
964
- size: fileList[i].size
965
- };
966
- if (info.isUpdate) {
967
- this._updateSrc(fileList[i].url, info.element, file);
968
- break;
969
- } else {
970
- this._produce(fileList[i].url, info.anchor, info.inputWidth, info.inputHeight, info.align, file, info.alt);
971
- }
972
- }
973
- }
974
-
975
- /**
976
- * @private
977
- * @description Uploads the image to the server.
978
- * @param {ImageInfo_image} info - Image upload info.
979
- * @param {FileList} files - List of image files.
980
- */
981
- _serverUpload(info, files) {
982
- if (!files) return;
983
-
984
- // server upload
985
- const imageUploadUrl = this.pluginOptions.uploadUrl;
986
- if (typeof imageUploadUrl === 'string' && imageUploadUrl.length > 0) {
987
- this.fileManager.upload(imageUploadUrl, this.pluginOptions.uploadHeaders, files, this.#UploadCallBack.bind(this, info), this._error.bind(this));
988
- } else {
989
- this._setBase64(files, info.anchor, info.inputWidth, info.inputHeight, info.align, info.alt, info.isUpdate);
990
- }
991
- }
992
-
993
- /**
994
- * @private
995
- * @description Converts an image file to Base64 and inserts it into the editor.
996
- * @param {FileList|File[]} files - List of image files.
997
- * @param {?Node} anchor - Optional anchor wrapping the image.
998
- * @param {string} width - Image width.
999
- * @param {string} height - Image height.
1000
- * @param {string} align - Image alignment.
1001
- * @param {string} alt - Alternative text.
1002
- * @param {boolean} isUpdate - Whether the image is being updated.
1003
- */
1004
- _setBase64(files, anchor, width, height, align, alt, isUpdate) {
1005
- try {
1006
- const filesLen = this.modal.isUpdate ? 1 : files.length;
1007
-
1008
- if (filesLen === 0) {
1009
- this.ui.hideLoading();
1010
- console.warn('[SUNEDITOR.image.base64.fail] cause : No applicable files');
1011
- return;
1012
- }
1013
-
1014
- this._base64RenderIndex = filesLen;
1015
- const filesStack = new Array(filesLen);
1016
-
1017
- if (this._resizing) {
1018
- this.inputX.value = width;
1019
- this.inputY.value = height;
1020
- }
1021
-
1022
- for (let i = 0, reader, file; i < filesLen; i++) {
1023
- reader = new FileReader();
1024
- file = files[i];
1025
-
1026
- reader.onload = function (on_reader, update, updateElement, on_file, index) {
1027
- filesStack[index] = {
1028
- result: on_reader.result,
1029
- file: on_file
1030
- };
1031
-
1032
- if (--this._base64RenderIndex === 0) {
1033
- this._onRenderBase64(update, filesStack, updateElement, anchor, width, height, align, alt);
1034
- this.ui.hideLoading();
1035
- }
1036
- }.bind(this, reader, isUpdate, this._element, file, i);
1037
- // se-ts-ignore
1038
- this._onRenderBase64;
1039
-
1040
- reader.readAsDataURL(file);
1041
- }
1042
- } catch (error) {
1043
- this.ui.hideLoading();
1044
- throw Error(`[SUNEDITOR.plugins.image._setBase64.fail] ${error.message}`);
1045
- }
1046
- }
1047
-
1048
- /**
1049
- * @private
1050
- * @description Inserts an image using a Base64-encoded string.
1051
- * @param {boolean} update - Whether the image is being updated.
1052
- * @param {Array<{result: string, file: { name: string, size: number }}>} filesStack - Stack of Base64-encoded files.
1053
- * - result: Image url or Base64-encoded string
1054
- * - file: File metadata ({ name: string, size: number })
1055
- * @param {HTMLImageElement} updateElement - The image element being updated.
1056
- * @param {?HTMLAnchorElement} anchor - Optional anchor wrapping the image.
1057
- * @param {string} width - Image width.
1058
- * @param {string} height - Image height.
1059
- * @param {string} align - Image alignment.
1060
- * @param {string} alt - Alternative text.
1061
- */
1062
- _onRenderBase64(update, filesStack, updateElement, anchor, width, height, align, alt) {
1063
- for (let i = 0, len = filesStack.length; i < len; i++) {
1064
- if (update) {
1065
- this._updateSrc(filesStack[i].result, updateElement, filesStack[i].file);
1066
- } else {
1067
- this._produce(filesStack[i].result, anchor, width, height, align, filesStack[i].file, alt);
1068
- }
1069
- }
1070
- }
1071
-
1072
- /**
1073
- * @private
1074
- * @description Wraps an image element with an anchor if provided.
1075
- * @param {Node} imgTag - The image element to be wrapped.
1076
- * @param {?Node} anchor - The anchor element to wrap around the image. If null, returns the image itself.
1077
- * @returns {Node} - The wrapped image inside the anchor or the original image element.
1078
- */
1079
- _setAnchor(imgTag, anchor) {
1080
- if (anchor) {
1081
- anchor.appendChild(imgTag);
1082
- return anchor;
1083
- }
1084
-
1085
- return imgTag;
1086
- }
1087
-
1088
- /**
1089
- * @private
1090
- * @description Handles errors during image upload and displays appropriate messages.
1091
- * @param {Object<string, *>} response - The error response from the server.
1092
- * @returns {Promise<void>}
1093
- */
1094
- async _error(response) {
1095
- const message = await this.triggerEvent('onImageUploadError', { error: response });
1096
- const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
1097
- this.ui.alertOpen(err, 'error');
1098
- console.error('[SUNEDITOR.plugin.image.error]', err);
1099
- }
1100
-
1101
- /**
1102
- * @description Handles the callback function for image upload completion.
1103
- * @param {ImageInfo_image} info - Image information.
1104
- * @param {XMLHttpRequest} xmlHttp - The XMLHttpRequest object.
1105
- */
1106
- async #UploadCallBack(info, xmlHttp) {
1107
- if ((await this.triggerEvent('imageUploadHandler', { xmlHttp, info })) === NO_EVENT) {
1108
- const response = JSON.parse(xmlHttp.responseText);
1109
- if (response.errorMessage) {
1110
- this._error(response);
1111
- } else {
1112
- this._register(info, response);
1113
- }
1114
- }
1115
- }
1116
-
1117
- #RemoveSelectedFiles() {
1118
- this.imgInputFile.value = '';
1119
- if (this.imgUrlFile) {
1120
- this.imgUrlFile.disabled = false;
1121
- this.previewSrc.style.textDecoration = '';
1122
- }
1123
-
1124
- // inputFile check
1125
- Modal.OnChangeFile(this.fileModalWrapper, []);
1126
- }
1127
-
1128
- #OnInputSize(xy, e) {
1129
- if (keyCodeMap.isSpace(e.code)) {
1130
- e.preventDefault();
1131
- return;
1132
- }
1133
-
1134
- if (xy === 'x' && this._onlyPercentage && e.target.value > 100) {
1135
- e.target.value = 100;
1136
- } else if (this.proportion.checked) {
1137
- const ratioSize = Figure.CalcRatio(this.inputX.value, this.inputY.value, this.sizeUnit, this._ratio);
1138
- if (xy === 'x') {
1139
- this.inputY.value = String(ratioSize.h);
1140
- } else {
1141
- this.inputX.value = String(ratioSize.w);
1142
- }
1143
- }
1144
- }
1145
-
1146
- #OnChangeRatio() {
1147
- this._ratio = this.proportion.checked
1148
- ? Figure.GetRatio(this.inputX.value, this.inputY.value, this.sizeUnit)
1149
- : {
1150
- w: 1,
1151
- h: 1
1152
- };
1153
- }
1154
-
1155
- #OnClickRevert() {
1156
- if (this._onlyPercentage) {
1157
- this.inputX.value = Number(this._origin_w) > 100 ? '100' : this._origin_w;
1158
- } else {
1159
- this.inputX.value = this._origin_w;
1160
- this.inputY.value = this._origin_h;
1161
- }
1162
- }
1163
-
1164
- #OnClickAsButton({ target }) {
1165
- this._activeAsInline(target.getAttribute('data-command') === 'asInline');
1166
- }
1167
-
1168
- #OnLinkPreview(e) {
1169
- const value = e.target.value.trim();
1170
- this._linkValue = this.previewSrc.textContent = !value
1171
- ? ''
1172
- : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
1173
- ? this.options.get('defaultUrlProtocol') + value
1174
- : !value.includes('://')
1175
- ? '/' + value
1176
- : value;
1177
- }
1178
-
1179
- #OnfileInputChange({ target }) {
1180
- if (!this.imgInputFile.value) {
1181
- this.imgUrlFile.disabled = false;
1182
- this.previewSrc.style.textDecoration = '';
1183
- } else {
1184
- this.imgUrlFile.disabled = true;
1185
- this.previewSrc.style.textDecoration = 'line-through';
1186
- }
1187
-
1188
- // inputFile check
1189
- Modal.OnChangeFile(this.fileModalWrapper, target.files);
1190
- }
1191
-
1192
- #OpenGallery() {
1193
- this.plugins.imageGallery.open(this.#SetUrlInput.bind(this));
1194
- }
1195
-
1196
- #SetUrlInput(target) {
1197
- this.altText.value = target.getAttribute('data-value') || target.alt;
1198
- this._linkValue = this.previewSrc.textContent = this.imgUrlFile.value = target.getAttribute('data-command') || target.src;
1199
- this.imgUrlFile.focus();
1200
- }
1201
-
1202
- #OnloadImg(oImg, _svgDefaultSize, container) {
1203
- // svg exception handling
1204
- if (oImg.offsetWidth === 0) this._applySize(_svgDefaultSize, '');
1205
- if (this.options.get('componentAutoSelect')) {
1206
- this.component.select(oImg, Image_.key);
1207
- } else {
1208
- if (!this.component.isInline(container)) {
1209
- const line = this.format.addLine(container, null);
1210
- if (line) this.selection.setRange(line, 0, line, 0);
1211
- } else {
1212
- const r = this.selection.getNearRange(container);
1213
- if (r) {
1214
- this.selection.setRange(r.container, r.offset, r.container, r.offset);
1215
- } else {
1216
- this.component.select(oImg, Image_.key);
1217
- }
1218
- }
1219
- }
1220
-
1221
- this.editor._iframeAutoHeight(this.editor.frameContext);
1222
- this.history.push(false);
1223
-
1224
- delete oImg.onload;
1225
- }
1226
- }
1227
-
1228
- /**
1229
- * @typedef {Object} ModalReturns_image
1230
- * @property {HTMLElement} html
1231
- * @property {HTMLElement} alignForm
1232
- * @property {HTMLElement} fileModalWrapper
1233
- * @property {HTMLInputElement} imgInputFile
1234
- * @property {HTMLInputElement} imgUrlFile
1235
- * @property {HTMLInputElement} altText
1236
- * @property {HTMLInputElement} captionCheckEl
1237
- * @property {HTMLElement} previewSrc
1238
- * @property {HTMLElement} tabs
1239
- * @property {HTMLButtonElement} galleryButton
1240
- * @property {HTMLInputElement} proportion
1241
- * @property {HTMLInputElement} inputX
1242
- * @property {HTMLInputElement} inputY
1243
- * @property {HTMLButtonElement} revertBtn
1244
- * @property {HTMLButtonElement} asBlock
1245
- * @property {HTMLButtonElement} asInline
1246
- * @property {HTMLButtonElement} fileRemoveBtn
1247
- *
1248
- * @param {__se__EditorCore} editor
1249
- * @param {*} pluginOptions
1250
- * @returns {ModalReturns_image}
1251
- */
1252
- function CreateHTML_modal({ lang, icons, plugins }, pluginOptions) {
1253
- const createFileInputHtml = !pluginOptions.createFileInput
1254
- ? ''
1255
- : /*html*/ `
1256
- <div class="se-modal-form">
1257
- <label>${lang.image_modal_file}</label>
1258
- ${Modal.CreateFileInput({ icons, lang }, pluginOptions)}
1259
- </div>`;
1260
-
1261
- const createUrlInputHtml = !pluginOptions.createUrlInput
1262
- ? ''
1263
- : /*html*/ `
1264
- <div class="se-modal-form">
1265
- <label>${lang.image_modal_url}</label>
1266
- <div class="se-modal-form-files">
1267
- <input class="se-input-form se-input-url" data-focus type="text" />
1268
- ${
1269
- plugins.imageGallery
1270
- ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button __se__gallery" aria-label="${lang.imageGallery}">
1271
- ${icons.image_gallery}
1272
- ${dom.utils.createTooltipInner(lang.imageGallery)}
1273
- </button>`
1274
- : ''
1275
- }
1276
- </div>
1277
- <pre class="se-link-preview"></pre>
1278
- </div>`;
1279
-
1280
- const canResizeHtml = !pluginOptions.canResize
1281
- ? ''
1282
- : /*html*/ `
1283
- <div class="se-modal-form">
1284
- <div class="se-modal-size-text">
1285
- <label class="size-w">${lang.width}</label>
1286
- <label class="se-modal-size-x">&nbsp;</label>
1287
- <label class="size-h">${lang.height}</label>
1288
- </div>
1289
- <input class="se-input-control _se_size_x" placeholder="auto" type="text" />
1290
- <label class="se-modal-size-x">x</label>
1291
- <input type="text" class="se-input-control _se_size_y" placeholder="auto" />
1292
- <label><input type="checkbox" class="se-modal-btn-check _se_check_proportion" checked/>&nbsp;${lang.proportion}</label>
1293
- <button type="button" aria-label="${lang.revert}" class="se-btn se-tooltip se-modal-btn-revert">
1294
- ${icons.revert}
1295
- ${dom.utils.createTooltipInner(lang.revert)}
1296
- </button>
1297
- </div>`;
1298
-
1299
- const useFormatTypeHtml = !pluginOptions.useFormatType
1300
- ? ''
1301
- : /*html*/ `
1302
- <div class="se-modal-form">
1303
- <div class="se-modal-flex-form">
1304
- <button type="button" data-command="asBlock" class="se-btn se-tooltip" aria-label="${lang.inlineStyle}">
1305
- ${icons.as_block}
1306
- ${dom.utils.createTooltipInner(lang.blockStyle)}
1307
- </button>
1308
- <button type="button" data-command="asInline" class="se-btn se-tooltip" aria-label="${lang.inlineStyle}">
1309
- ${icons.as_inline}
1310
- ${dom.utils.createTooltipInner(lang.inlineStyle)}
1311
- </button>
1312
- </div>
1313
- </div>`;
1314
-
1315
- const html = /*html*/ `
1316
- <div class="se-modal-header">
1317
- <button type="button" data-command="close" class="se-btn se-close-btn close" title="${lang.close}" aria-label="${lang.close}">${icons.cancel}</button>
1318
- <span class="se-modal-title">${lang.image_modal_title}</span>
1319
- </div>
1320
- <div class="se-modal-tabs">
1321
- <button type="button" class="_se_tab_link active" data-tab-link="image">${lang.image}</button>
1322
- <button type="button" class="_se_tab_link" data-tab-link="url">${lang.link}</button>
1323
- </div>
1324
- <form method="post" enctype="multipart/form-data">
1325
- <div class="_se_tab_content _se_tab_content_image">
1326
- <div class="se-modal-body">
1327
- ${createFileInputHtml}
1328
- ${createUrlInputHtml}
1329
- <div style="border-bottom: 1px dashed #ccc;"></div>
1330
- <div class="se-modal-form">
1331
- <label>${lang.image_modal_altText}</label><input class="se-input-form _se_image_alt" type="text" />
1332
- </div>
1333
- ${canResizeHtml}
1334
- ${useFormatTypeHtml}
1335
- <div class="se-modal-form se-modal-form-footer">
1336
- <label><input type="checkbox" class="se-modal-btn-check _se_image_check_caption" />&nbsp;${lang.caption}</label>
1337
- </div>
1338
- </div>
1339
- </div>
1340
- <div class="se-anchor-editor _se_tab_content _se_tab_content_url" style="display: none;">
1341
- </div>
1342
- <div class="se-modal-footer">
1343
- <div class="se-figure-align">
1344
- <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="none" checked>${lang.basic}</label>
1345
- <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="left">${lang.left}</label>
1346
- <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="center">${lang.center}</label>
1347
- <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="right">${lang.right}</label>
1348
- </div>
1349
- <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}"><span>${lang.submitButton}</span></button>
1350
- </div>
1351
- </form>`;
1352
-
1353
- const content = dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
1354
-
1355
- return {
1356
- html: content,
1357
- alignForm: content.querySelector('.se-figure-align'),
1358
- fileModalWrapper: content.querySelector('.se-flex-input-wrapper'),
1359
- imgInputFile: content.querySelector('.__se__file_input'),
1360
- imgUrlFile: content.querySelector('.se-input-url'),
1361
- altText: content.querySelector('._se_image_alt'),
1362
- captionCheckEl: content.querySelector('._se_image_check_caption'),
1363
- previewSrc: content.querySelector('._se_tab_content_image .se-link-preview'),
1364
- tabs: content.querySelector('.se-modal-tabs'),
1365
- galleryButton: content.querySelector('.__se__gallery'),
1366
- proportion: content.querySelector('._se_check_proportion'),
1367
- inputX: content.querySelector('._se_size_x'),
1368
- inputY: content.querySelector('._se_size_y'),
1369
- revertBtn: content.querySelector('.se-modal-btn-revert'),
1370
- asBlock: content.querySelector('[data-command="asBlock"]'),
1371
- asInline: content.querySelector('[data-command="asInline"]'),
1372
- fileRemoveBtn: content.querySelector('.se-file-remove')
1373
- };
1374
- }
1375
-
1376
- export default Image_;
1
+ import EditorInjector from '../../editorInjector';
2
+ import { Modal, Figure, FileManager, ModalAnchorEditor } from '../../modules';
3
+ import { dom, numbers, env, keyCodeMap } from '../../helper';
4
+ const { NO_EVENT } = env;
5
+
6
+ /**
7
+ * @typedef {import('../../events').ImageInfo} ImageInfo_image
8
+ */
9
+
10
+ /**
11
+ * @typedef {import('../../modules/Figure').FigureControls} FigureControls_image
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} ImagePluginOptions
16
+ * @property {boolean} [canResize=true] - Whether the image element can be resized.
17
+ * @property {boolean} [showHeightInput=true] - Whether to display the height input field.
18
+ * @property {string} [defaultWidth="auto"] - The default width of the image. If a number is provided, "px" will be appended.
19
+ * @property {string} [defaultHeight="auto"] - The default height of the image. If a number is provided, "px" will be appended.
20
+ * @property {boolean} [percentageOnlySize=false] - Whether to allow only percentage-based sizing.
21
+ * @property {boolean} [createFileInput=true] - Whether to create a file input element for image uploads.
22
+ * @property {boolean} [createUrlInput=true] - Whether to create a URL input element for image insertion.
23
+ * @property {string} [uploadUrl] - The URL endpoint for image file uploads.
24
+ * @property {Object<string, string>} [uploadHeaders] - Additional headers to include in the file upload request.
25
+ * @property {number} [uploadSizeLimit] - The total upload size limit in bytes.
26
+ * @property {number} [uploadSingleSizeLimit] - The single file upload size limit in bytes.
27
+ * @property {boolean} [allowMultiple=false] - Whether multiple image uploads are allowed.
28
+ * @property {string} [acceptedFormats="image/*"] - The accepted file formats for image uploads.
29
+ * @property {boolean} [useFormatType=true] - Whether to enable format type selection (block or inline).
30
+ * @property {string} [defaultFormatType="block"] - The default image format type ("block" or "inline").
31
+ * @property {boolean} [keepFormatType=false] - Whether to retain the chosen format type after image insertion.
32
+ * @property {boolean} [linkEnableFileUpload] - Whether to enable file uploads for linked images.
33
+ * @property {FigureControls_image} [controls] - Figure controls.
34
+ */
35
+
36
+ /**
37
+ * @class
38
+ * @description Image plugin.
39
+ * - This plugin provides image insertion functionality within the editor, supporting both file upload and URL input.
40
+ */
41
+ class Image_ extends EditorInjector {
42
+ static key = 'image';
43
+ static type = 'modal';
44
+ static className = '';
45
+ /**
46
+ * @this {Image_}
47
+ * @param {Element} node - The node to check.
48
+ * @returns {Element|null} Returns a node if the node is a valid component.
49
+ */
50
+ static component(node) {
51
+ const compNode = dom.check.isFigure(node) || (/^span$/i.test(node.nodeName) && dom.utils.hasClass(node, 'se-component')) ? node.firstElementChild : node;
52
+ return /^IMG$/i.test(compNode?.nodeName) ? compNode : dom.check.isAnchor(compNode) && /^IMG$/i.test(compNode?.firstElementChild?.nodeName) ? compNode?.firstElementChild : null;
53
+ }
54
+
55
+ /**
56
+ * @constructor
57
+ * @param {__se__EditorCore} editor - The root editor instance
58
+ * @param {ImagePluginOptions} pluginOptions
59
+ */
60
+ constructor(editor, pluginOptions) {
61
+ // plugin bisic properties
62
+ super(editor);
63
+ this.title = this.lang.image;
64
+ this.icon = 'image';
65
+
66
+ this.pluginOptions = {
67
+ canResize: pluginOptions.canResize === undefined ? true : pluginOptions.canResize,
68
+ showHeightInput: pluginOptions.showHeightInput === undefined ? true : !!pluginOptions.showHeightInput,
69
+ defaultWidth: !pluginOptions.defaultWidth ? 'auto' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
70
+ defaultHeight: !pluginOptions.defaultHeight ? 'auto' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
71
+ percentageOnlySize: !!pluginOptions.percentageOnlySize,
72
+ createFileInput: pluginOptions.createFileInput === undefined ? true : pluginOptions.createFileInput,
73
+ createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
74
+ uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
75
+ uploadHeaders: pluginOptions.uploadHeaders || null,
76
+ uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
77
+ uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
78
+ allowMultiple: !!pluginOptions.allowMultiple,
79
+ acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'image/*' : pluginOptions.acceptedFormats.trim() || 'image/*',
80
+ useFormatType: pluginOptions.useFormatType ?? true,
81
+ defaultFormatType: ['block', 'inline'].includes(pluginOptions.defaultFormatType) ? pluginOptions.defaultFormatType : 'block',
82
+ keepFormatType: pluginOptions.keepFormatType ?? false
83
+ };
84
+
85
+ // create HTML
86
+ const sizeUnit = this.pluginOptions.percentageOnlySize ? '%' : 'px';
87
+ const modalEl = CreateHTML_modal(editor, this.pluginOptions);
88
+ const ctrlAs = this.pluginOptions.useFormatType ? 'as' : '';
89
+ const figureControls =
90
+ pluginOptions.controls || !this.pluginOptions.canResize
91
+ ? [[ctrlAs, 'mirror_h', 'mirror_v', 'align', 'caption', 'edit', 'revert', 'copy', 'remove']]
92
+ : [
93
+ [ctrlAs, 'resize_auto,100,75,50', 'rotate_l', 'rotate_r', 'mirror_h', 'mirror_v'],
94
+ ['edit', 'align', 'caption', 'revert', 'copy', 'remove']
95
+ ];
96
+
97
+ // show align
98
+ this.alignForm = modalEl.alignForm;
99
+ if (!figureControls.some((subArray) => subArray.includes('align'))) this.alignForm.style.display = 'none';
100
+
101
+ // modules
102
+ const Link = this.plugins.link ? this.plugins.link.pluginOptions : {};
103
+ this.anchor = new ModalAnchorEditor(this, modalEl.html, {
104
+ textToDisplay: false,
105
+ title: true,
106
+ openNewWindow: Link.openNewWindow,
107
+ relList: Link.relList,
108
+ defaultRel: Link.defaultRel,
109
+ noAutoPrefix: Link.noAutoPrefix,
110
+ enableFileUpload: pluginOptions.linkEnableFileUpload
111
+ });
112
+ this.modal = new Modal(this, modalEl.html);
113
+ this.figure = new Figure(this, figureControls, {
114
+ sizeUnit: sizeUnit
115
+ });
116
+ this.fileManager = new FileManager(this, {
117
+ query: 'img',
118
+ loadHandler: this.events.onImageLoad,
119
+ eventHandler: this.events.onImageAction
120
+ });
121
+
122
+ // members
123
+ this.fileModalWrapper = modalEl.fileModalWrapper;
124
+ this.imgInputFile = modalEl.imgInputFile;
125
+ this.imgUrlFile = modalEl.imgUrlFile;
126
+ this.focusElement = this.imgInputFile || this.imgUrlFile;
127
+ this.altText = modalEl.altText;
128
+ this.captionCheckEl = modalEl.captionCheckEl;
129
+ this.captionEl = this.captionCheckEl?.parentElement;
130
+ this.previewSrc = modalEl.previewSrc;
131
+ this.sizeUnit = sizeUnit;
132
+ this.as = 'block';
133
+ this.proportion = null;
134
+ this.inputX = null;
135
+ this.inputY = null;
136
+ this._linkElement = null;
137
+ this._linkValue = '';
138
+ this._align = 'none';
139
+ this._svgDefaultSize = '30%';
140
+ this._base64RenderIndex = 0;
141
+ this._element = null;
142
+ this._cover = null;
143
+ this._container = null;
144
+ this._caption = null;
145
+ this._ratio = {
146
+ w: 1,
147
+ h: 1
148
+ };
149
+ this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
150
+ this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
151
+ this._resizing = this.pluginOptions.canResize;
152
+ this._onlyPercentage = this.pluginOptions.percentageOnlySize;
153
+ this._nonResizing = !this._resizing || !this.pluginOptions.showHeightInput || this._onlyPercentage;
154
+
155
+ // init
156
+ this.eventManager.addEvent(modalEl.tabs, 'click', this.#OpenTab.bind(this));
157
+ if (this.imgInputFile) this.eventManager.addEvent(modalEl.fileRemoveBtn, 'click', this.#RemoveSelectedFiles.bind(this));
158
+ if (this.imgUrlFile) this.eventManager.addEvent(this.imgUrlFile, 'input', this.#OnLinkPreview.bind(this));
159
+ if (this.imgInputFile && this.imgUrlFile) this.eventManager.addEvent(this.imgInputFile, 'change', this.#OnfileInputChange.bind(this));
160
+
161
+ const galleryButton = modalEl.galleryButton;
162
+ if (galleryButton) this.eventManager.addEvent(galleryButton, 'click', this.#OpenGallery.bind(this));
163
+
164
+ if (this._resizing) {
165
+ this.proportion = modalEl.proportion;
166
+ this.inputX = modalEl.inputX;
167
+ this.inputY = modalEl.inputY;
168
+ this.inputX.value = this.pluginOptions.defaultWidth;
169
+ this.inputY.value = this.pluginOptions.defaultHeight;
170
+
171
+ const ratioChange = this.#OnChangeRatio.bind(this);
172
+ this.eventManager.addEvent(this.inputX, 'keyup', this.#OnInputSize.bind(this, 'x'));
173
+ this.eventManager.addEvent(this.inputY, 'keyup', this.#OnInputSize.bind(this, 'y'));
174
+ this.eventManager.addEvent(this.inputX, 'change', ratioChange);
175
+ this.eventManager.addEvent(this.inputY, 'change', ratioChange);
176
+ this.eventManager.addEvent(this.proportion, 'change', ratioChange);
177
+ this.eventManager.addEvent(modalEl.revertBtn, 'click', this.#OnClickRevert.bind(this));
178
+ }
179
+
180
+ if (this.pluginOptions.useFormatType) {
181
+ this.as = this.pluginOptions.defaultFormatType;
182
+ this.asBlock = modalEl.asBlock;
183
+ this.asInline = modalEl.asInline;
184
+ this.eventManager.addEvent([this.asBlock, this.asInline], 'click', this.#OnClickAsButton.bind(this));
185
+ }
186
+ }
187
+
188
+ /**
189
+ * @editorMethod Modules.Modal
190
+ * @description Executes the method that is called when a "Modal" module's is opened.
191
+ */
192
+ open() {
193
+ this.modal.open();
194
+ }
195
+
196
+ /**
197
+ * @editorMethod Modules.Controller(Figure)
198
+ * @description Executes the method that is called when a target component is edited.
199
+ */
200
+ edit() {
201
+ this.modal.open();
202
+ }
203
+
204
+ /**
205
+ * @editorMethod Modules.Modal
206
+ * @description Executes the method that is called when a plugin's modal is opened.
207
+ * @param {boolean} isUpdate "Indicates whether the modal is for editing an existing component (true) or registering a new one (false)."
208
+ */
209
+ on(isUpdate) {
210
+ if (!isUpdate) {
211
+ if (this._resizing) {
212
+ this.inputX.value = this._origin_w = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
213
+ this.inputY.value = this._origin_h = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
214
+ }
215
+ if (this.imgInputFile && this.pluginOptions.allowMultiple) this.imgInputFile.setAttribute('multiple', 'multiple');
216
+ } else {
217
+ if (this.imgInputFile && this.pluginOptions.allowMultiple) this.imgInputFile.removeAttribute('multiple');
218
+ }
219
+
220
+ this.anchor.on(isUpdate);
221
+ }
222
+
223
+ /**
224
+ * @editorMethod Editor.EventManager
225
+ * @description Executes the event function of "paste" or "drop".
226
+ * @param {Object} params { frameContext, event, file }
227
+ * @param {__se__FrameContext} params.frameContext Frame context
228
+ * @param {ClipboardEvent} params.event Event object
229
+ * @param {File} params.file File object
230
+ * @returns {boolean} - If return false, the file upload will be canceled
231
+ */
232
+ onFilePasteAndDrop({ file }) {
233
+ if (!/^image/.test(file.type)) return;
234
+
235
+ this.submitFile([file]);
236
+ this.editor.focus();
237
+
238
+ return false;
239
+ }
240
+
241
+ /**
242
+ * @editorMethod Modules.Modal
243
+ * @description This function is called when a form within a modal window is "submit".
244
+ * @returns {Promise<boolean>} Success or failure
245
+ */
246
+ async modalAction() {
247
+ this._align = /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_image_radio"]:checked')).value;
248
+
249
+ if (this.modal.isUpdate) {
250
+ this._update(this.inputX?.value, this.inputY?.value);
251
+ this.history.push(false);
252
+ }
253
+
254
+ if (this.imgInputFile && this.imgInputFile.files.length > 0) {
255
+ return await this.submitFile(this.imgInputFile.files);
256
+ } else if (this.imgUrlFile && this._linkValue.length > 0) {
257
+ return await this.submitURL(this._linkValue);
258
+ }
259
+
260
+ return false;
261
+ }
262
+
263
+ /**
264
+ * @editorMethod Editor.core
265
+ * @description This method is used to validate and preserve the format of the component within the editor.
266
+ * - It ensures that the structure and attributes of the element are maintained and secure.
267
+ * - The method checks if the element is already wrapped in a valid container and updates its attributes if necessary.
268
+ * - If the element isn't properly contained, a new container is created to retain the format.
269
+ * @returns {{query: string, method: (element: HTMLImageElement) => void}} The format retention object containing the query and method to process the element.
270
+ * - query: The selector query to identify the relevant elements (in this case, 'audio').
271
+ * - method:The function to execute on the element to validate and preserve its format.
272
+ * - The function takes the element as an argument, checks if it is contained correctly, and applies necessary adjustments.
273
+ */
274
+ retainFormat() {
275
+ return {
276
+ query: 'img',
277
+ method: (element) => {
278
+ const figureInfo = Figure.GetContainer(element);
279
+ if (figureInfo && figureInfo.container && (figureInfo.cover || figureInfo.inlineCover)) return;
280
+
281
+ this._ready(element);
282
+ this._fileCheck(this._origin_w, this._origin_h);
283
+ }
284
+ };
285
+ }
286
+
287
+ /**
288
+ * @editorMethod Modules.Modal
289
+ * @description This function is called before the modal window is opened, but before it is closed.
290
+ */
291
+ init() {
292
+ Modal.OnChangeFile(this.fileModalWrapper, []);
293
+ if (this.imgInputFile) this.imgInputFile.value = '';
294
+ if (this.imgUrlFile) this._linkValue = this.previewSrc.textContent = this.imgUrlFile.value = '';
295
+ if (this.imgInputFile && this.imgUrlFile) {
296
+ this.imgUrlFile.disabled = false;
297
+ this.previewSrc.style.textDecoration = '';
298
+ }
299
+
300
+ this.altText.value = '';
301
+ /** @type {HTMLInputElement} */ (this.modal.form.querySelector('input[name="suneditor_image_radio"][value="none"]')).checked = true;
302
+ this.captionCheckEl.checked = false;
303
+ this._element = null;
304
+ this._ratio = {
305
+ w: 1,
306
+ h: 1
307
+ };
308
+ this.#OpenTab('init');
309
+
310
+ if (this._resizing) {
311
+ this.inputX.value = this.pluginOptions.defaultWidth === 'auto' ? '' : this.pluginOptions.defaultWidth;
312
+ this.inputY.value = this.pluginOptions.defaultHeight === 'auto' ? '' : this.pluginOptions.defaultHeight;
313
+ this.proportion.checked = true;
314
+ }
315
+
316
+ if (this.pluginOptions.useFormatType) {
317
+ this._activeAsInline((this.pluginOptions.keepFormatType ? this.as : this.pluginOptions.defaultFormatType) === 'inline');
318
+ }
319
+
320
+ this.anchor.init();
321
+ }
322
+
323
+ /**
324
+ * @editorMethod Editor.Component
325
+ * @description Executes the method that is called when a component of a plugin is selected.
326
+ * @param {HTMLElement} target Target component element
327
+ */
328
+ select(target) {
329
+ this._ready(target);
330
+ }
331
+
332
+ /**
333
+ * @private
334
+ * @description Prepares the component for selection.
335
+ * - Ensures that the controller is properly positioned and initialized.
336
+ * - Prevents duplicate event handling if the component is already selected.
337
+ * @param {HTMLElement} target - The selected element.
338
+ */
339
+ _ready(target) {
340
+ if (!target) return;
341
+ const figureInfo = this.figure.open(target, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: false });
342
+ this.anchor.set(dom.check.isAnchor(target.parentNode) ? target.parentNode : null);
343
+
344
+ this._linkElement = this.anchor.currentTarget;
345
+ this._element = target;
346
+ this._cover = figureInfo.cover;
347
+ this._container = figureInfo.container;
348
+ this._caption = figureInfo.caption;
349
+ this._align = figureInfo.align;
350
+ target.style.float = '';
351
+
352
+ this._origin_w = String(figureInfo.originWidth || figureInfo.w || '');
353
+ this._origin_h = String(figureInfo.originHeight || figureInfo.h || '');
354
+ this.altText.value = this._element.alt;
355
+
356
+ if (this.imgUrlFile) this._linkValue = this.previewSrc.textContent = this.imgUrlFile.value = this._element.src;
357
+
358
+ /** @type {HTMLInputElement} */
359
+ const activeAlign = this.modal.form.querySelector('input[name="suneditor_image_radio"][value="' + this._align + '"]') || this.modal.form.querySelector('input[name="suneditor_image_radio"][value="none"]');
360
+ activeAlign.checked = true;
361
+ this.captionCheckEl.checked = !!this._caption;
362
+
363
+ if (!this._resizing) return;
364
+
365
+ const percentageRotation = this._onlyPercentage && this.figure.isVertical;
366
+ let w = percentageRotation ? '' : figureInfo.width;
367
+ if (this._onlyPercentage) {
368
+ w = numbers.get(w, 2);
369
+ if (w > 100) w = 100;
370
+ }
371
+ this.inputX.value = String(w === 'auto' ? '' : w);
372
+
373
+ if (!this._onlyPercentage) {
374
+ const h = percentageRotation ? '' : figureInfo.height;
375
+ this.inputY.value = String(h === 'auto' ? '' : h);
376
+ }
377
+
378
+ this.proportion.checked = true;
379
+ this.inputX.disabled = percentageRotation ? true : false;
380
+ this.inputY.disabled = percentageRotation ? true : false;
381
+ this.proportion.disabled = percentageRotation ? true : false;
382
+
383
+ this._ratio = this.proportion.checked
384
+ ? figureInfo.ratio
385
+ : {
386
+ w: 1,
387
+ h: 1
388
+ };
389
+
390
+ if (this.pluginOptions.useFormatType) {
391
+ this._activeAsInline(this.component.isInline(figureInfo.container));
392
+ }
393
+ }
394
+
395
+ /**
396
+ * @editorMethod Editor.Component
397
+ * @description Method to delete a component of a plugin, called by the "FileManager", "Controller" module.
398
+ * @param {HTMLElement} target Target element
399
+ * @returns {Promise<void>}
400
+ */
401
+ async destroy(target) {
402
+ const targetEl = target || this._element;
403
+ const container = dom.query.getParentElement(targetEl, Figure.is) || targetEl;
404
+ const focusEl = container.previousElementSibling || container.nextElementSibling;
405
+ const emptyDiv = container.parentNode;
406
+
407
+ const message = await this.triggerEvent('onImageDeleteBefore', { element: targetEl, container, align: this._align, alt: this.altText.value, url: this._linkValue });
408
+ if (message === false) return;
409
+
410
+ dom.utils.removeItem(container);
411
+ this.init();
412
+
413
+ if (emptyDiv !== this.editor.frameContext.get('wysiwyg')) {
414
+ this.nodeTransform.removeAllParents(
415
+ emptyDiv,
416
+ function (current) {
417
+ return current.childNodes.length === 0;
418
+ },
419
+ null
420
+ );
421
+ }
422
+
423
+ // focus
424
+ this.editor.focusEdge(focusEl);
425
+ this.history.push(false);
426
+ }
427
+
428
+ /**
429
+ * @private
430
+ * @description Retrieves the current image information.
431
+ * @returns {*} - The image data.
432
+ */
433
+ _getInfo() {
434
+ return {
435
+ element: this._element,
436
+ anchor: this.anchor.create(true),
437
+ inputWidth: this.inputX?.value || '',
438
+ inputHeight: this.inputY?.value || '',
439
+ align: this._align,
440
+ isUpdate: this.modal.isUpdate,
441
+ alt: this.altText.value
442
+ };
443
+ }
444
+
445
+ /**
446
+ * @private
447
+ * @description Toggles between block and inline image format.
448
+ * @param {boolean} isInline - Whether the image should be inline.
449
+ */
450
+ _activeAsInline(isInline) {
451
+ if (isInline) {
452
+ dom.utils.addClass(this.asInline, 'on');
453
+ dom.utils.removeClass(this.asBlock, 'on');
454
+ this.as = 'inline';
455
+ // buttns
456
+ if (this.alignForm) this.alignForm.style.display = 'none';
457
+ // caption
458
+ if (this.captionEl) this.captionEl.style.display = 'none';
459
+ } else {
460
+ dom.utils.addClass(this.asBlock, 'on');
461
+ dom.utils.removeClass(this.asInline, 'on');
462
+ this.as = 'block';
463
+ // buttns
464
+ if (this.alignForm) this.alignForm.style.display = '';
465
+ // caption
466
+ if (this.captionEl) this.captionEl.style.display = '';
467
+ }
468
+ }
469
+
470
+ /**
471
+ * @description Create an "image" component using the provided files.
472
+ * @param {FileList|File[]} fileList File object list
473
+ * @returns {Promise<boolean>} If return false, the file upload will be canceled
474
+ */
475
+ async submitFile(fileList) {
476
+ if (fileList.length === 0) return false;
477
+
478
+ let fileSize = 0;
479
+ const files = [];
480
+ const slngleSizeLimit = this.pluginOptions.uploadSingleSizeLimit;
481
+ for (let i = 0, len = fileList.length, f, s; i < len; i++) {
482
+ f = fileList[i];
483
+ if (!/image/i.test(f.type)) continue;
484
+
485
+ s = f.size;
486
+ if (slngleSizeLimit && slngleSizeLimit > s) {
487
+ const err = '[SUNEDITOR.imageUpload.fail] Size of uploadable single file: ' + slngleSizeLimit / 1000 + 'KB';
488
+ const message = await this.triggerEvent('onImageUploadError', {
489
+ error: err,
490
+ limitSize: slngleSizeLimit,
491
+ uploadSize: s,
492
+ file: f
493
+ });
494
+
495
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
496
+
497
+ return false;
498
+ }
499
+
500
+ files.push(f);
501
+ fileSize += s;
502
+ }
503
+
504
+ const limitSize = this.pluginOptions.uploadSizeLimit;
505
+ const currentSize = this.fileManager.getSize();
506
+ if (limitSize > 0 && fileSize + currentSize > limitSize) {
507
+ const err = '[SUNEDITOR.imageUpload.fail] Size of uploadable total images: ' + limitSize / 1000 + 'KB';
508
+ const message = await this.triggerEvent('onImageUploadError', {
509
+ error: err,
510
+ limitSize,
511
+ currentSize,
512
+ uploadSize: fileSize
513
+ });
514
+
515
+ this.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
516
+
517
+ return false;
518
+ }
519
+
520
+ const imgInfo = { files, ...this._getInfo() };
521
+ const handler = function (infos, newInfos) {
522
+ infos = newInfos || infos;
523
+ this._serverUpload(infos, infos.files);
524
+ }.bind(this, imgInfo);
525
+ // se-ts-ignore
526
+ this._serverUpload;
527
+
528
+ const result = await this.triggerEvent('onImageUploadBefore', {
529
+ info: imgInfo,
530
+ handler
531
+ });
532
+
533
+ if (result === undefined) return true;
534
+ if (result === false) return false;
535
+ if (result !== null && typeof result === 'object') handler(result);
536
+
537
+ if (result === true || result === NO_EVENT) handler(null);
538
+ }
539
+
540
+ /**
541
+ * @description Create an "image" component using the provided url.
542
+ * @param {string} url File url
543
+ * @returns {Promise<boolean>} If return false, the file upload will be canceled
544
+ */
545
+ async submitURL(url) {
546
+ if (!url) url = this._linkValue;
547
+ if (!url) return false;
548
+
549
+ const file = { name: url.split('/').pop(), size: 0 };
550
+ const imgInfo = {
551
+ url,
552
+ files: file,
553
+ ...this._getInfo()
554
+ };
555
+
556
+ const handler = function (infos, newInfos) {
557
+ infos = newInfos || infos;
558
+ const infoUrl = infos.url;
559
+ if (this.modal.isUpdate) this._updateSrc(infoUrl, infos.element, infos.files);
560
+ else this._produce(infoUrl, infos.anchor, infos.inputWidth, infos.inputHeight, infos.align, infos.files, infos.alt);
561
+ }.bind(this, imgInfo);
562
+
563
+ const result = await this.triggerEvent('onImageUploadBefore', {
564
+ info: imgInfo,
565
+ handler
566
+ });
567
+
568
+ if (result === undefined) return true;
569
+ if (result === false) return false;
570
+ if (result !== null && typeof result === 'object') handler(result);
571
+
572
+ if (result === true || result === NO_EVENT) handler(null);
573
+
574
+ return true;
575
+ }
576
+
577
+ /**
578
+ * @private
579
+ * @description Updates the selected image size, alt text, and caption.
580
+ * @param {string} width - New image width.
581
+ * @param {string} height - New image height.
582
+ */
583
+ _update(width, height) {
584
+ if (!width) width = this.inputX?.value || 'auto';
585
+ if (!height) height = this.inputY?.value || 'auto';
586
+
587
+ let imageEl = this._element;
588
+
589
+ // as (block | inline)
590
+ if ((this.as === 'block' && !this._cover) || (this.as === 'inline' && this._cover)) {
591
+ imageEl = this.figure.convertAsFormat(imageEl, this.as);
592
+ }
593
+
594
+ // --- update image ---
595
+ const cover = this._cover;
596
+ const container = this._container === this._cover ? null : this._container;
597
+
598
+ // check size
599
+ let changeSize;
600
+ const x = numbers.is(width) ? width + this.sizeUnit : width;
601
+ const y = numbers.is(height) ? height + this.sizeUnit : height;
602
+ if (/%$/.test(imageEl.style.width)) {
603
+ changeSize = x !== container.style.width || y !== container.style.height;
604
+ } else {
605
+ changeSize = x !== imageEl.style.width || y !== imageEl.style.height;
606
+ }
607
+
608
+ // alt
609
+ imageEl.alt = this.altText.value;
610
+
611
+ // caption
612
+ let modifiedCaption = false;
613
+ if (this.captionCheckEl.checked) {
614
+ if (!this._caption) {
615
+ this._caption = Figure.CreateCaption(cover, this.lang.caption);
616
+ modifiedCaption = true;
617
+ }
618
+ } else {
619
+ if (this._caption) {
620
+ dom.utils.removeItem(this._caption);
621
+ this._caption = null;
622
+ modifiedCaption = true;
623
+ }
624
+ }
625
+
626
+ // link
627
+ let isNewAnchor = false;
628
+ const anchor = this.anchor.create(true);
629
+ if (anchor) {
630
+ if (this._linkElement !== anchor || !container.contains(anchor)) {
631
+ this._linkElement = anchor.cloneNode(false);
632
+ cover.insertBefore(this._setAnchor(imageEl, this._linkElement), this._caption);
633
+ isNewAnchor = true;
634
+ }
635
+ } else if (this._linkElement !== null) {
636
+ if (cover.contains(this._linkElement)) {
637
+ const newEl = imageEl.cloneNode(true);
638
+ cover.removeChild(this._linkElement);
639
+ cover.insertBefore(newEl, this._caption);
640
+ imageEl = newEl;
641
+ }
642
+ }
643
+
644
+ // size
645
+ if (this._resizing && changeSize) {
646
+ this._applySize(width, height);
647
+ }
648
+
649
+ if (isNewAnchor) {
650
+ dom.utils.removeItem(anchor);
651
+ }
652
+
653
+ // transform
654
+ if (modifiedCaption || (!this._onlyPercentage && changeSize)) {
655
+ if (/\d+/.test(imageEl.style.height) || (this.figure.isVertical && this.captionCheckEl.checked)) {
656
+ if (/auto|%$/.test(width) || /auto|%$/.test(height)) {
657
+ this.figure.deleteTransform(imageEl);
658
+ } else {
659
+ this.figure.setTransform(imageEl, width, height, 0);
660
+ }
661
+ }
662
+ }
663
+
664
+ // align
665
+ this.figure.setAlign(imageEl, this._align);
666
+
667
+ // select
668
+ imageEl.onload = () => {
669
+ this.select(imageEl);
670
+ };
671
+
672
+ this._ready(imageEl);
673
+ }
674
+
675
+ /**
676
+ * @private
677
+ * @description Validates the image size and applies necessary transformations.
678
+ * @param {string} width - The width of the image.
679
+ * @param {string} height - The height of the image.
680
+ */
681
+ _fileCheck(width, height) {
682
+ if (!width) width = this.inputX?.value || 'auto';
683
+ if (!height) height = this.inputY?.value || 'auto';
684
+
685
+ let imageEl = this._element;
686
+ let cover = this._cover;
687
+ let inlineCover = null;
688
+ let container = this._container === this._cover ? null : this._container;
689
+ let isNewContainer = false;
690
+
691
+ if (!cover || !container) {
692
+ isNewContainer = true;
693
+ imageEl = this._element.cloneNode(true);
694
+ const figureInfo =
695
+ this.pluginOptions.useFormatType && width !== 'auto' && (/^span$/i.test(this._element.parentElement?.nodeName) || this.format.isLine(this._element.parentElement))
696
+ ? Figure.CreateInlineContainer(imageEl, 'se-image-container')
697
+ : Figure.CreateContainer(imageEl, 'se-image-container');
698
+ cover = figureInfo.cover;
699
+ container = figureInfo.container;
700
+ inlineCover = figureInfo.inlineCover;
701
+ this.figure.open(imageEl, { nonResizing: true, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
702
+ }
703
+
704
+ // alt
705
+ imageEl.alt = this.altText.value;
706
+
707
+ // caption
708
+ let modifiedCaption = false;
709
+ if (!inlineCover) {
710
+ if (this.captionCheckEl.checked) {
711
+ if (!this._caption || isNewContainer) {
712
+ this._caption = Figure.CreateCaption(cover, this.lang.caption);
713
+ modifiedCaption = true;
714
+ }
715
+ } else {
716
+ if (this._caption) {
717
+ dom.utils.removeItem(this._caption);
718
+ this._caption = null;
719
+ modifiedCaption = true;
720
+ }
721
+ }
722
+ }
723
+
724
+ // link
725
+ let isNewAnchor = null;
726
+ const anchor = this.anchor.create(true);
727
+ if (anchor) {
728
+ if (this._linkElement !== anchor || (isNewContainer && !container.contains(anchor))) {
729
+ this._linkElement = anchor.cloneNode(false);
730
+ cover.insertBefore(this._setAnchor(imageEl, this._linkElement), this._caption);
731
+ isNewAnchor = this._element;
732
+ }
733
+ } else if (this._linkElement !== null) {
734
+ if (cover.contains(this._linkElement)) {
735
+ const newEl = imageEl.cloneNode(true);
736
+ cover.removeChild(this._linkElement);
737
+ cover.insertBefore(newEl, this._caption);
738
+ imageEl = newEl;
739
+ }
740
+ }
741
+
742
+ if (isNewContainer) {
743
+ imageEl = this._element;
744
+ this.figure.retainFigureFormat(container, this._element, isNewAnchor ? anchor : null, this.fileManager);
745
+ this._element = imageEl = container.querySelector('img');
746
+ this._cover = cover;
747
+ this._container = container;
748
+ }
749
+
750
+ // size
751
+ imageEl.style.width = '';
752
+ imageEl.style.height = '';
753
+ imageEl.removeAttribute('width');
754
+ imageEl.removeAttribute('height');
755
+ this._applySize(width, height);
756
+
757
+ if (isNewAnchor) {
758
+ if (!isNewContainer) {
759
+ dom.utils.removeItem(anchor);
760
+ } else {
761
+ dom.utils.removeItem(isNewAnchor);
762
+ if (dom.query.getListChildren(anchor, (current) => /IMG/i.test(current.tagName)).length === 0) {
763
+ dom.utils.removeItem(anchor);
764
+ }
765
+ }
766
+ }
767
+
768
+ // transform
769
+ if (modifiedCaption || !this._onlyPercentage) {
770
+ if (/\d+/.test(imageEl.style.height) || (this.figure.isVertical && this.captionCheckEl.checked)) {
771
+ if (/auto|%$/.test(width) || /auto|%$/.test(height)) {
772
+ this.figure.deleteTransform(imageEl);
773
+ } else {
774
+ this.figure.setTransform(imageEl, width, height, 0);
775
+ }
776
+ }
777
+ }
778
+
779
+ // align
780
+ this.figure.setAlign(imageEl, this._align);
781
+ }
782
+
783
+ /**
784
+ * @description Opens a specific tab inside the modal.
785
+ * @param {MouseEvent|string} e - The event object or tab name.
786
+ * @returns {boolean} - Whether the tab was successfully opened.
787
+ */
788
+ #OpenTab(e) {
789
+ const modalForm = this.modal.form;
790
+ const targetElement = typeof e === 'string' ? modalForm.querySelector('._se_tab_link') : dom.query.getEventTarget(e);
791
+
792
+ if (!/^BUTTON$/i.test(targetElement.tagName)) {
793
+ return false;
794
+ }
795
+
796
+ // Declare all variables
797
+ const tabName = targetElement.getAttribute('data-tab-link');
798
+ let i;
799
+
800
+ // Get all elements with class="tabcontent" and hide them
801
+ const tabContent = /** @type {HTMLCollectionOf<HTMLElement>}*/ (modalForm.getElementsByClassName('_se_tab_content'));
802
+ for (i = 0; i < tabContent.length; i++) {
803
+ tabContent[i].style.display = 'none';
804
+ }
805
+
806
+ // Get all elements with class="tablinks" and remove the class "active"
807
+ const tabLinks = modalForm.getElementsByClassName('_se_tab_link');
808
+ for (i = 0; i < tabLinks.length; i++) {
809
+ dom.utils.removeClass(tabLinks[i], 'active');
810
+ }
811
+
812
+ // Show the current tab, and add an "active" class to the button that opened the tab
813
+ /** @type {HTMLElement}*/ (modalForm.querySelector('._se_tab_content_' + tabName)).style.display = 'block';
814
+ dom.utils.addClass(targetElement, 'active');
815
+
816
+ // focus
817
+ if (e !== 'init') {
818
+ if (tabName === 'image') {
819
+ this.focusElement.focus();
820
+ } else if (tabName === 'url') {
821
+ this.anchor.urlInput.focus();
822
+ }
823
+ }
824
+
825
+ return false;
826
+ }
827
+
828
+ /**
829
+ * @private
830
+ * @description Creates a new image component based on provided parameters.
831
+ * @param {string} src - The image source URL.
832
+ * @param {?Node} anchor - Optional anchor wrapping the image.
833
+ * @param {string} width - Image width.
834
+ * @param {string} height - Image height.
835
+ * @param {string} align - Image alignment.
836
+ * @param {{name: string, size: number}} file - File metadata.
837
+ * @param {string} alt - Alternative text.
838
+ */
839
+ _produce(src, anchor, width, height, align, file, alt) {
840
+ if (this.as !== 'inline') {
841
+ this.create(src, anchor, width, height, align, file, alt);
842
+ } else {
843
+ this.createInline(src, anchor, width, height, file, alt);
844
+ }
845
+ }
846
+
847
+ /**
848
+ * @private
849
+ * @description Applies the specified width and height to the image.
850
+ * @param {string} w - Image width.
851
+ * @param {string} h - Image height.
852
+ */
853
+ _applySize(w, h) {
854
+ if (!w) w = this.inputX?.value || this.pluginOptions.defaultWidth;
855
+ if (!h) h = this.inputY?.value || this.pluginOptions.defaultHeight;
856
+ if (this._onlyPercentage) {
857
+ if (!w) w = '100%';
858
+ else if (/%$/.test(w)) w += '%';
859
+ }
860
+ this.figure.setSize(w, h);
861
+ }
862
+
863
+ /**
864
+ * @description Creates a new image component, wraps it in a figure container with an optional anchor,
865
+ * - applies size and alignment settings, and inserts it into the editor.
866
+ * @param {string} src - The URL of the image to be inserted.
867
+ * @param {?Node} anchor - An optional anchor element to wrap the image. If provided, a clone is used.
868
+ * @param {string} width - The width value to be applied to the image.
869
+ * @param {string} height - The height value to be applied to the image.
870
+ * @param {string} align - The alignment setting for the image (e.g., 'left', 'center', 'right').
871
+ * @param {{name: string, size: number}} file - File metadata associated with the image
872
+ * @param {string} alt - The alternative text for the image.
873
+ */
874
+ create(src, anchor, width, height, align, file, alt) {
875
+ /** @type {HTMLImageElement} */
876
+ const oImg = dom.utils.createElement('IMG');
877
+ oImg.src = src;
878
+ oImg.alt = alt;
879
+ anchor = this._setAnchor(oImg, anchor ? anchor.cloneNode(false) : null);
880
+
881
+ const figureInfo = Figure.CreateContainer(anchor, 'se-image-container');
882
+ const cover = figureInfo.cover;
883
+ const container = figureInfo.container;
884
+
885
+ // caption
886
+ if (this.captionCheckEl.checked) {
887
+ this._caption = Figure.CreateCaption(cover, this.lang.caption);
888
+ }
889
+
890
+ this._element = oImg;
891
+ this._cover = cover;
892
+ this._container = container;
893
+ this.figure.open(oImg, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
894
+
895
+ // set size
896
+ this._applySize(width, height);
897
+
898
+ // align
899
+ this.figure.setAlign(oImg, align);
900
+
901
+ this.fileManager.setFileData(oImg, file);
902
+
903
+ oImg.onload = this.#OnloadImg.bind(this, oImg, this._svgDefaultSize, container);
904
+ this.component.insert(container, { skipCharCount: false, skipSelection: !this.options.get('componentAutoSelect'), skipHistory: false });
905
+ }
906
+
907
+ /**
908
+ * @description Creates a new inline image component, wraps it in an inline figure container with an optional anchor,
909
+ * - applies size settings, and inserts it into the editor.
910
+ * @param {string} src - The URL of the image to be inserted.
911
+ * @param {?Node} anchor - An optional anchor element to wrap the image. If provided, a clone is used.
912
+ * @param {string} width - The width value to be applied to the image.
913
+ * @param {string} height - The height value to be applied to the image.
914
+ * @param {{name: string, size: number}} file - File metadata associated with the image
915
+ * @param {string} alt - The alternative text for the image.
916
+ */
917
+ createInline(src, anchor, width, height, file, alt) {
918
+ /** @type {HTMLImageElement} */
919
+ const oImg = dom.utils.createElement('IMG');
920
+ oImg.src = src;
921
+ oImg.alt = alt;
922
+ anchor = this._setAnchor(oImg, anchor ? anchor.cloneNode(false) : null);
923
+
924
+ const figureInfo = Figure.CreateInlineContainer(anchor, 'se-image-container');
925
+ const container = figureInfo.container;
926
+
927
+ this._element = oImg;
928
+ this._container = container;
929
+ this.figure.open(oImg, { nonResizing: this._nonResizing, nonSizeInfo: false, nonBorder: false, figureTarget: false, __fileManagerInfo: true });
930
+
931
+ // set size
932
+ this._applySize(width, height);
933
+
934
+ this.fileManager.setFileData(oImg, file);
935
+
936
+ oImg.onload = this.#OnloadImg.bind(this, oImg, this._svgDefaultSize, container);
937
+ this.component.insert(container, { skipCharCount: false, skipSelection: true, skipHistory: false });
938
+ }
939
+
940
+ /**
941
+ * @private
942
+ * @description Updates the image source URL.
943
+ * @param {string} src - The new image source.
944
+ * @param {HTMLImageElement} element - The image element.
945
+ * @param {{ name: string, size: number }} file - File metadata.
946
+ */
947
+ _updateSrc(src, element, file) {
948
+ element.src = src;
949
+ this.fileManager.setFileData(element, file);
950
+ this.component.select(element, Image_.key);
951
+ }
952
+
953
+ /**
954
+ * @private
955
+ * @description Registers the uploaded image and inserts it into the editor.
956
+ * @param {ImageInfo_image} info - Image info.
957
+ * @param {Object<string, *>} response - Server response data.
958
+ */
959
+ _register(info, response) {
960
+ const fileList = response.result;
961
+
962
+ for (let i = 0, len = fileList.length, file; i < len; i++) {
963
+ file = {
964
+ name: fileList[i].name,
965
+ size: fileList[i].size
966
+ };
967
+ if (info.isUpdate) {
968
+ this._updateSrc(fileList[i].url, info.element, file);
969
+ break;
970
+ } else {
971
+ this._produce(fileList[i].url, info.anchor, info.inputWidth, info.inputHeight, info.align, file, info.alt);
972
+ }
973
+ }
974
+ }
975
+
976
+ /**
977
+ * @private
978
+ * @description Uploads the image to the server.
979
+ * @param {ImageInfo_image} info - Image upload info.
980
+ * @param {FileList} files - List of image files.
981
+ */
982
+ _serverUpload(info, files) {
983
+ if (!files) return;
984
+
985
+ // server upload
986
+ const imageUploadUrl = this.pluginOptions.uploadUrl;
987
+ if (typeof imageUploadUrl === 'string' && imageUploadUrl.length > 0) {
988
+ this.fileManager.upload(imageUploadUrl, this.pluginOptions.uploadHeaders, files, this.#UploadCallBack.bind(this, info), this._error.bind(this));
989
+ } else {
990
+ this._setBase64(files, info.anchor, info.inputWidth, info.inputHeight, info.align, info.alt, info.isUpdate);
991
+ }
992
+ }
993
+
994
+ /**
995
+ * @private
996
+ * @description Converts an image file to Base64 and inserts it into the editor.
997
+ * @param {FileList|File[]} files - List of image files.
998
+ * @param {?Node} anchor - Optional anchor wrapping the image.
999
+ * @param {string} width - Image width.
1000
+ * @param {string} height - Image height.
1001
+ * @param {string} align - Image alignment.
1002
+ * @param {string} alt - Alternative text.
1003
+ * @param {boolean} isUpdate - Whether the image is being updated.
1004
+ */
1005
+ _setBase64(files, anchor, width, height, align, alt, isUpdate) {
1006
+ try {
1007
+ const filesLen = this.modal.isUpdate ? 1 : files.length;
1008
+
1009
+ if (filesLen === 0) {
1010
+ this.ui.hideLoading();
1011
+ console.warn('[SUNEDITOR.image.base64.fail] cause : No applicable files');
1012
+ return;
1013
+ }
1014
+
1015
+ this._base64RenderIndex = filesLen;
1016
+ const filesStack = new Array(filesLen);
1017
+
1018
+ if (this._resizing) {
1019
+ this.inputX.value = width;
1020
+ this.inputY.value = height;
1021
+ }
1022
+
1023
+ for (let i = 0, reader, file; i < filesLen; i++) {
1024
+ reader = new FileReader();
1025
+ file = files[i];
1026
+
1027
+ reader.onload = function (on_reader, update, updateElement, on_file, index) {
1028
+ filesStack[index] = {
1029
+ result: on_reader.result,
1030
+ file: on_file
1031
+ };
1032
+
1033
+ if (--this._base64RenderIndex === 0) {
1034
+ this._onRenderBase64(update, filesStack, updateElement, anchor, width, height, align, alt);
1035
+ this.ui.hideLoading();
1036
+ }
1037
+ }.bind(this, reader, isUpdate, this._element, file, i);
1038
+ // se-ts-ignore
1039
+ this._onRenderBase64;
1040
+
1041
+ reader.readAsDataURL(file);
1042
+ }
1043
+ } catch (error) {
1044
+ this.ui.hideLoading();
1045
+ throw Error(`[SUNEDITOR.plugins.image._setBase64.fail] ${error.message}`);
1046
+ }
1047
+ }
1048
+
1049
+ /**
1050
+ * @private
1051
+ * @description Inserts an image using a Base64-encoded string.
1052
+ * @param {boolean} update - Whether the image is being updated.
1053
+ * @param {Array<{result: string, file: { name: string, size: number }}>} filesStack - Stack of Base64-encoded files.
1054
+ * - result: Image url or Base64-encoded string
1055
+ * - file: File metadata ({ name: string, size: number })
1056
+ * @param {HTMLImageElement} updateElement - The image element being updated.
1057
+ * @param {?HTMLAnchorElement} anchor - Optional anchor wrapping the image.
1058
+ * @param {string} width - Image width.
1059
+ * @param {string} height - Image height.
1060
+ * @param {string} align - Image alignment.
1061
+ * @param {string} alt - Alternative text.
1062
+ */
1063
+ _onRenderBase64(update, filesStack, updateElement, anchor, width, height, align, alt) {
1064
+ for (let i = 0, len = filesStack.length; i < len; i++) {
1065
+ if (update) {
1066
+ this._updateSrc(filesStack[i].result, updateElement, filesStack[i].file);
1067
+ } else {
1068
+ this._produce(filesStack[i].result, anchor, width, height, align, filesStack[i].file, alt);
1069
+ }
1070
+ }
1071
+ }
1072
+
1073
+ /**
1074
+ * @private
1075
+ * @description Wraps an image element with an anchor if provided.
1076
+ * @param {Node} imgTag - The image element to be wrapped.
1077
+ * @param {?Node} anchor - The anchor element to wrap around the image. If null, returns the image itself.
1078
+ * @returns {Node} - The wrapped image inside the anchor or the original image element.
1079
+ */
1080
+ _setAnchor(imgTag, anchor) {
1081
+ if (anchor) {
1082
+ anchor.appendChild(imgTag);
1083
+ return anchor;
1084
+ }
1085
+
1086
+ return imgTag;
1087
+ }
1088
+
1089
+ /**
1090
+ * @private
1091
+ * @description Handles errors during image upload and displays appropriate messages.
1092
+ * @param {Object<string, *>} response - The error response from the server.
1093
+ * @returns {Promise<void>}
1094
+ */
1095
+ async _error(response) {
1096
+ const message = await this.triggerEvent('onImageUploadError', { error: response });
1097
+ const err = message === NO_EVENT ? response.errorMessage : message || response.errorMessage;
1098
+ this.ui.alertOpen(err, 'error');
1099
+ console.error('[SUNEDITOR.plugin.image.error]', err);
1100
+ }
1101
+
1102
+ /**
1103
+ * @description Handles the callback function for image upload completion.
1104
+ * @param {ImageInfo_image} info - Image information.
1105
+ * @param {XMLHttpRequest} xmlHttp - The XMLHttpRequest object.
1106
+ */
1107
+ async #UploadCallBack(info, xmlHttp) {
1108
+ if ((await this.triggerEvent('imageUploadHandler', { xmlHttp, info })) === NO_EVENT) {
1109
+ const response = JSON.parse(xmlHttp.responseText);
1110
+ if (response.errorMessage) {
1111
+ this._error(response);
1112
+ } else {
1113
+ this._register(info, response);
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ #RemoveSelectedFiles() {
1119
+ this.imgInputFile.value = '';
1120
+ if (this.imgUrlFile) {
1121
+ this.imgUrlFile.disabled = false;
1122
+ this.previewSrc.style.textDecoration = '';
1123
+ }
1124
+
1125
+ // inputFile check
1126
+ Modal.OnChangeFile(this.fileModalWrapper, []);
1127
+ }
1128
+
1129
+ #OnInputSize(xy, e) {
1130
+ if (keyCodeMap.isSpace(e.code)) {
1131
+ e.preventDefault();
1132
+ return;
1133
+ }
1134
+
1135
+ if (xy === 'x' && this._onlyPercentage && e.target.value > 100) {
1136
+ e.target.value = 100;
1137
+ } else if (this.proportion.checked) {
1138
+ const ratioSize = Figure.CalcRatio(this.inputX.value, this.inputY.value, this.sizeUnit, this._ratio);
1139
+ if (xy === 'x') {
1140
+ this.inputY.value = String(ratioSize.h);
1141
+ } else {
1142
+ this.inputX.value = String(ratioSize.w);
1143
+ }
1144
+ }
1145
+ }
1146
+
1147
+ #OnChangeRatio() {
1148
+ this._ratio = this.proportion.checked
1149
+ ? Figure.GetRatio(this.inputX.value, this.inputY.value, this.sizeUnit)
1150
+ : {
1151
+ w: 1,
1152
+ h: 1
1153
+ };
1154
+ }
1155
+
1156
+ #OnClickRevert() {
1157
+ if (this._onlyPercentage) {
1158
+ this.inputX.value = Number(this._origin_w) > 100 ? '100' : this._origin_w;
1159
+ } else {
1160
+ this.inputX.value = this._origin_w;
1161
+ this.inputY.value = this._origin_h;
1162
+ }
1163
+ }
1164
+
1165
+ #OnClickAsButton({ target }) {
1166
+ this._activeAsInline(target.getAttribute('data-command') === 'asInline');
1167
+ }
1168
+
1169
+ #OnLinkPreview(e) {
1170
+ const value = e.target.value.trim();
1171
+ this._linkValue = this.previewSrc.textContent = !value
1172
+ ? ''
1173
+ : this.options.get('defaultUrlProtocol') && !value.includes('://') && value.indexOf('#') !== 0
1174
+ ? this.options.get('defaultUrlProtocol') + value
1175
+ : !value.includes('://')
1176
+ ? '/' + value
1177
+ : value;
1178
+ }
1179
+
1180
+ #OnfileInputChange({ target }) {
1181
+ if (!this.imgInputFile.value) {
1182
+ this.imgUrlFile.disabled = false;
1183
+ this.previewSrc.style.textDecoration = '';
1184
+ } else {
1185
+ this.imgUrlFile.disabled = true;
1186
+ this.previewSrc.style.textDecoration = 'line-through';
1187
+ }
1188
+
1189
+ // inputFile check
1190
+ Modal.OnChangeFile(this.fileModalWrapper, target.files);
1191
+ }
1192
+
1193
+ #OpenGallery() {
1194
+ this.plugins.imageGallery.open(this.#SetUrlInput.bind(this));
1195
+ }
1196
+
1197
+ #SetUrlInput(target) {
1198
+ this.altText.value = target.getAttribute('data-value') || target.alt;
1199
+ this._linkValue = this.previewSrc.textContent = this.imgUrlFile.value = target.getAttribute('data-command') || target.src;
1200
+ this.imgUrlFile.focus();
1201
+ }
1202
+
1203
+ #OnloadImg(oImg, _svgDefaultSize, container) {
1204
+ // svg exception handling
1205
+ if (oImg.offsetWidth === 0) this._applySize(_svgDefaultSize, '');
1206
+ if (this.options.get('componentAutoSelect')) {
1207
+ this.component.select(oImg, Image_.key);
1208
+ } else {
1209
+ if (!this.component.isInline(container)) {
1210
+ const line = this.format.addLine(container, null);
1211
+ if (line) this.selection.setRange(line, 0, line, 0);
1212
+ } else {
1213
+ const r = this.selection.getNearRange(container);
1214
+ if (r) {
1215
+ this.selection.setRange(r.container, r.offset, r.container, r.offset);
1216
+ } else {
1217
+ this.component.select(oImg, Image_.key);
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ this.editor._iframeAutoHeight(this.editor.frameContext);
1223
+ this.history.push(false);
1224
+
1225
+ delete oImg.onload;
1226
+ }
1227
+ }
1228
+
1229
+ /**
1230
+ * @typedef {Object} ModalReturns_image
1231
+ * @property {HTMLElement} html
1232
+ * @property {HTMLElement} alignForm
1233
+ * @property {HTMLElement} fileModalWrapper
1234
+ * @property {HTMLInputElement} imgInputFile
1235
+ * @property {HTMLInputElement} imgUrlFile
1236
+ * @property {HTMLInputElement} altText
1237
+ * @property {HTMLInputElement} captionCheckEl
1238
+ * @property {HTMLElement} previewSrc
1239
+ * @property {HTMLElement} tabs
1240
+ * @property {HTMLButtonElement} galleryButton
1241
+ * @property {HTMLInputElement} proportion
1242
+ * @property {HTMLInputElement} inputX
1243
+ * @property {HTMLInputElement} inputY
1244
+ * @property {HTMLButtonElement} revertBtn
1245
+ * @property {HTMLButtonElement} asBlock
1246
+ * @property {HTMLButtonElement} asInline
1247
+ * @property {HTMLButtonElement} fileRemoveBtn
1248
+ *
1249
+ * @param {__se__EditorCore} editor
1250
+ * @param {*} pluginOptions
1251
+ * @returns {ModalReturns_image}
1252
+ */
1253
+ function CreateHTML_modal({ lang, icons, plugins }, pluginOptions) {
1254
+ const createFileInputHtml = !pluginOptions.createFileInput
1255
+ ? ''
1256
+ : /*html*/ `
1257
+ <div class="se-modal-form">
1258
+ <label>${lang.image_modal_file}</label>
1259
+ ${Modal.CreateFileInput({ icons, lang }, pluginOptions)}
1260
+ </div>`;
1261
+
1262
+ const createUrlInputHtml = !pluginOptions.createUrlInput
1263
+ ? ''
1264
+ : /*html*/ `
1265
+ <div class="se-modal-form">
1266
+ <label>${lang.image_modal_url}</label>
1267
+ <div class="se-modal-form-files">
1268
+ <input class="se-input-form se-input-url" data-focus type="text" />
1269
+ ${
1270
+ plugins.imageGallery
1271
+ ? `<button type="button" class="se-btn se-tooltip se-modal-files-edge-button __se__gallery" aria-label="${lang.imageGallery}">
1272
+ ${icons.image_gallery}
1273
+ ${dom.utils.createTooltipInner(lang.imageGallery)}
1274
+ </button>`
1275
+ : ''
1276
+ }
1277
+ </div>
1278
+ <pre class="se-link-preview"></pre>
1279
+ </div>`;
1280
+
1281
+ const canResizeHtml = !pluginOptions.canResize
1282
+ ? ''
1283
+ : /*html*/ `
1284
+ <div class="se-modal-form">
1285
+ <div class="se-modal-size-text">
1286
+ <label class="size-w">${lang.width}</label>
1287
+ <label class="se-modal-size-x">&nbsp;</label>
1288
+ <label class="size-h">${lang.height}</label>
1289
+ </div>
1290
+ <input class="se-input-control _se_size_x" placeholder="auto" type="text" />
1291
+ <label class="se-modal-size-x">x</label>
1292
+ <input type="text" class="se-input-control _se_size_y" placeholder="auto" />
1293
+ <label><input type="checkbox" class="se-modal-btn-check _se_check_proportion" checked/>&nbsp;${lang.proportion}</label>
1294
+ <button type="button" aria-label="${lang.revert}" class="se-btn se-tooltip se-modal-btn-revert">
1295
+ ${icons.revert}
1296
+ ${dom.utils.createTooltipInner(lang.revert)}
1297
+ </button>
1298
+ </div>`;
1299
+
1300
+ const useFormatTypeHtml = !pluginOptions.useFormatType
1301
+ ? ''
1302
+ : /*html*/ `
1303
+ <div class="se-modal-form">
1304
+ <div class="se-modal-flex-form">
1305
+ <button type="button" data-command="asBlock" class="se-btn se-tooltip" aria-label="${lang.inlineStyle}">
1306
+ ${icons.as_block}
1307
+ ${dom.utils.createTooltipInner(lang.blockStyle)}
1308
+ </button>
1309
+ <button type="button" data-command="asInline" class="se-btn se-tooltip" aria-label="${lang.inlineStyle}">
1310
+ ${icons.as_inline}
1311
+ ${dom.utils.createTooltipInner(lang.inlineStyle)}
1312
+ </button>
1313
+ </div>
1314
+ </div>`;
1315
+
1316
+ const html = /*html*/ `
1317
+ <div class="se-modal-header">
1318
+ <button type="button" data-command="close" class="se-btn se-close-btn close" title="${lang.close}" aria-label="${lang.close}">${icons.cancel}</button>
1319
+ <span class="se-modal-title">${lang.image_modal_title}</span>
1320
+ </div>
1321
+ <div class="se-modal-tabs">
1322
+ <button type="button" class="_se_tab_link active" data-tab-link="image">${lang.image}</button>
1323
+ <button type="button" class="_se_tab_link" data-tab-link="url">${lang.link}</button>
1324
+ </div>
1325
+ <form method="post" enctype="multipart/form-data">
1326
+ <div class="_se_tab_content _se_tab_content_image">
1327
+ <div class="se-modal-body">
1328
+ ${createFileInputHtml}
1329
+ ${createUrlInputHtml}
1330
+ <div style="border-bottom: 1px dashed #ccc;"></div>
1331
+ <div class="se-modal-form">
1332
+ <label>${lang.image_modal_altText}</label><input class="se-input-form _se_image_alt" type="text" />
1333
+ </div>
1334
+ ${canResizeHtml}
1335
+ ${useFormatTypeHtml}
1336
+ <div class="se-modal-form se-modal-form-footer">
1337
+ <label><input type="checkbox" class="se-modal-btn-check _se_image_check_caption" />&nbsp;${lang.caption}</label>
1338
+ </div>
1339
+ </div>
1340
+ </div>
1341
+ <div class="se-anchor-editor _se_tab_content _se_tab_content_url" style="display: none;">
1342
+ </div>
1343
+ <div class="se-modal-footer">
1344
+ <div class="se-figure-align">
1345
+ <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="none" checked>${lang.basic}</label>
1346
+ <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="left">${lang.left}</label>
1347
+ <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="center">${lang.center}</label>
1348
+ <label><input type="radio" name="suneditor_image_radio" class="se-modal-btn-radio" value="right">${lang.right}</label>
1349
+ </div>
1350
+ <button type="submit" class="se-btn-primary" title="${lang.submitButton}" aria-label="${lang.submitButton}"><span>${lang.submitButton}</span></button>
1351
+ </div>
1352
+ </form>`;
1353
+
1354
+ const content = dom.utils.createElement('DIV', { class: 'se-modal-content' }, html);
1355
+
1356
+ return {
1357
+ html: content,
1358
+ alignForm: content.querySelector('.se-figure-align'),
1359
+ fileModalWrapper: content.querySelector('.se-flex-input-wrapper'),
1360
+ imgInputFile: content.querySelector('.__se__file_input'),
1361
+ imgUrlFile: content.querySelector('.se-input-url'),
1362
+ altText: content.querySelector('._se_image_alt'),
1363
+ captionCheckEl: content.querySelector('._se_image_check_caption'),
1364
+ previewSrc: content.querySelector('._se_tab_content_image .se-link-preview'),
1365
+ tabs: content.querySelector('.se-modal-tabs'),
1366
+ galleryButton: content.querySelector('.__se__gallery'),
1367
+ proportion: content.querySelector('._se_check_proportion'),
1368
+ inputX: content.querySelector('._se_size_x'),
1369
+ inputY: content.querySelector('._se_size_y'),
1370
+ revertBtn: content.querySelector('.se-modal-btn-revert'),
1371
+ asBlock: content.querySelector('[data-command="asBlock"]'),
1372
+ asInline: content.querySelector('[data-command="asInline"]'),
1373
+ fileRemoveBtn: content.querySelector('.se-file-remove')
1374
+ };
1375
+ }
1376
+
1377
+ export default Image_;