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,842 +1,856 @@
1
- /**
2
- * @fileoverview Component class
3
- */
4
-
5
- import CoreInjector from '../../editorInjector/_core';
6
- import { dom, env, numbers, unicode, keyCodeMap, converter } from '../../helper';
7
- import { Figure, _DragHandle } from '../../modules';
8
-
9
- const { _w, ON_OVER_COMPONENT, isMobile } = env;
10
- const DIR_KEYCODE = /^(3[7-9]|40)$/;
11
- const DIR_UP_KEYCODE = /^3[7-8]$/;
12
-
13
- /**
14
- * @typedef {Omit<Component & Partial<__se__EditorInjector>, 'component'>} ComponentThis
15
- */
16
-
17
- /**
18
- * @constructor
19
- * @this {ComponentThis}
20
- * @description Class for managing components such as images and tables that are not in line format
21
- * @param {__se__EditorCore} editor - The root editor instance
22
- */
23
- function Component(editor) {
24
- CoreInjector.call(this, editor);
25
-
26
- /**
27
- * @description The current component information, used copy, cut, and keydown events
28
- * @type {__se__ComponentInfo}
29
- */
30
- this.info = null;
31
-
32
- /**
33
- * @description Component is selected
34
- * @type {boolean}
35
- */
36
- this.isSelected = false;
37
-
38
- /**
39
- * @description Currently selected component target
40
- * @type {Node|null}
41
- */
42
- this.currentTarget = null;
43
-
44
- /**
45
- * @description Currently selected component plugin instance
46
- * @type {*}
47
- */
48
- this.currentPlugin = null;
49
-
50
- /**
51
- * @description Currently selected component plugin name
52
- * @type {*}
53
- */
54
- this.currentPluginName = '';
55
-
56
- /**
57
- * @description Currently selected component information
58
- * @type {__se__ComponentInfo|null}
59
- */
60
- this.currentInfo = null;
61
-
62
- /** @type {Object<string, (...args: *) => *>} */
63
- this.__globalEvents = {
64
- copy: OnCopy_component.bind(this),
65
- cut: OnCut_component.bind(this),
66
- keydown: OnKeyDown_component.bind(this),
67
- mousedown: CloseListener_mousedown.bind(this)
68
- };
69
- /** @type {__se__GlobalEventInfo|void} */
70
- this._bindClose_copy = null;
71
- /** @type {__se__GlobalEventInfo|void} */
72
- this._bindClose_cut = null;
73
- /** @type {__se__GlobalEventInfo|void} */
74
- this._bindClose_keydown = null;
75
- /** @type {__se__GlobalEventInfo|void} */
76
- this._bindClose_mousedown = null;
77
- /** @type {__se__GlobalEventInfo|void} */
78
- this._bindClose_touchstart = null;
79
- /** @type {boolean} */
80
- this.__selectionSelected = false;
81
-
82
- this.editor.applyFrameRoots((e) => {
83
- // drag
84
- const dragHandle = dom.utils.createElement('DIV', { class: 'se-drag-handle', draggable: 'true' }, this.icons.selection);
85
- e.get('wrapper').appendChild(dragHandle);
86
- this.eventManager.addEvent(dragHandle, 'mouseenter', OnDragEnter.bind(this));
87
- this.eventManager.addEvent(dragHandle, 'mouseleave', OnDragLeave.bind(this));
88
- this.eventManager.addEvent(dragHandle, 'dragstart', OnDragStart.bind(this));
89
- this.eventManager.addEvent(dragHandle, 'dragend', OnDragEnd.bind(this));
90
- this.eventManager.addEvent(dragHandle, 'click', OnDragClick.bind(this));
91
- });
92
- }
93
-
94
- Component.prototype = {
95
- /**
96
- * @this {ComponentThis}
97
- * @description Inserts an element and returns it. (Used for elements: table, hr, image, video)
98
- * - If "element" is "HR", inserts and returns the new line.
99
- * @param {Node} element Element to be inserted
100
- * @param {Object} [options] Options
101
- * @param {boolean} [options.skipCharCount=false] If true, it will be inserted even if "frameOptions.get('charCounter_max')" is exceeded.
102
- * @param {boolean} [options.skipSelection=false] If true, do not automatically select the inserted component.
103
- * @param {boolean} [options.skipHistory=false] If true, do not push to history.
104
- * @returns {HTMLElement} The inserted element or new line (for HR)
105
- */
106
- insert(element, { skipCharCount, skipSelection, skipHistory } = {}) {
107
- if (this.editor.frameContext.get('isReadOnly') || (!skipCharCount && !this.char.check(element))) {
108
- return null;
109
- }
110
-
111
- const r = this.html.remove();
112
- const isInline = this.isInline(element);
113
- this.selection.getRangeAndAddLine(this.selection.getRange(), r.container);
114
- const selectionNode = this.selection.getNode();
115
- let oNode = null;
116
- let formatEl = this.format.getLine(selectionNode, null);
117
-
118
- if (dom.check.isListCell(formatEl)) {
119
- this.html.insertNode(element, { afterNode: isInline ? null : selectionNode === formatEl ? null : r.container.nextSibling, skipCharCount: true });
120
- if (!isInline && !element.nextSibling) element.parentNode.appendChild(dom.utils.createElement('BR'));
121
- } else {
122
- if (!isInline && this.selection.getRange().collapsed && (r.container.nodeType === 3 || dom.check.isBreak(r.container))) {
123
- const depthFormat = dom.query.getParentElement(r.container, this.format.isBlock.bind(this.format));
124
- oNode = this.nodeTransform.split(r.container, r.offset, !depthFormat ? 0 : dom.query.getNodeDepth(depthFormat) + 1);
125
- if (oNode) formatEl = /** @type {HTMLElement} */ (oNode.previousSibling);
126
- }
127
- this.html.insertNode(element, { afterNode: isInline ? null : this.format.isBlock(formatEl) ? null : formatEl, skipCharCount: true });
128
- if (!isInline && formatEl && dom.check.isZeroWidth(formatEl)) dom.utils.removeItem(formatEl);
129
- }
130
-
131
- if (isInline) {
132
- const empty = dom.utils.createTextNode(unicode.zeroWidthSpace);
133
- element.parentNode.insertBefore(empty, element.nextSibling);
134
- }
135
-
136
- if (!skipHistory) this.history.push(false);
137
-
138
- if (!skipSelection) {
139
- this.selection.setRange(element, 0, element, 0);
140
- const fileComponentInfo = this.get(element);
141
- if (fileComponentInfo) {
142
- this.select(fileComponentInfo.target, fileComponentInfo.pluginName);
143
- } else if (oNode) {
144
- oNode = dom.query.getEdgeChildNodes(oNode, null).sc || oNode;
145
- this.selection.setRange(oNode, 0, oNode, 0);
146
- }
147
- }
148
-
149
- // document type
150
- if (this.editor.frameContext.has('documentType-use-header')) {
151
- this.editor.frameContext.get('documentType').reHeader();
152
- }
153
-
154
- return /** @type {HTMLElement} */ (oNode || element);
155
- },
156
-
157
- /**
158
- * @this {ComponentThis}
159
- * @description Gets the file component and that plugin name
160
- * - return: {target, component, pluginName} | null
161
- * @param {Node} element Target element (figure tag, component div, file tag)
162
- * @returns {__se__ComponentInfo|null}
163
- */
164
- get(element) {
165
- if (!element) return null;
166
-
167
- let target;
168
- let pluginName = '';
169
- let options = {};
170
- let isFile = false;
171
- let launcher = null;
172
-
173
- if (this.is(element)) {
174
- if (dom.utils.hasClass(element, 'se-component') && !dom.utils.hasClass(element, 'se-inline-component')) element = /** @type {HTMLElement} */ (element).firstElementChild || element;
175
- if (/^FIGURE$/i.test(element.nodeName)) element = /** @type {HTMLElement} */ (element).firstElementChild;
176
- if (!element) return null;
177
-
178
- const comp = this.editor._componentManager.map((f) => f(element)).find((e) => e);
179
- if (!comp) return null;
180
- target = comp.target;
181
- pluginName = comp.pluginName;
182
- options = comp.options;
183
- launcher = comp.launcher;
184
- }
185
-
186
- if (!target && element.nodeName) {
187
- if (this.__isFiles(element)) {
188
- isFile = true;
189
- }
190
- const comp = this.editor._componentManager.map((f) => f(element)).find((e) => e);
191
- if (!comp) return null;
192
- target = comp.target;
193
- pluginName = comp.pluginName;
194
- options = comp.options;
195
- launcher = comp.launcher;
196
- }
197
-
198
- if (!target) {
199
- return null;
200
- }
201
-
202
- const figureInfo = Figure.GetContainer(target);
203
- const container = figureInfo.container || figureInfo.cover || target;
204
- return (this.info = {
205
- target,
206
- pluginName,
207
- options,
208
- container: container,
209
- cover: figureInfo.cover,
210
- inlineCover: figureInfo.inlineCover,
211
- caption: figureInfo.caption,
212
- isFile,
213
- launcher,
214
- isInputType: dom.utils.hasClass(container, 'se-input-component')
215
- });
216
- },
217
-
218
- /**
219
- * @this {ComponentThis}
220
- * @description The component(media, file component, table, etc) is selected and the resizing module is called.
221
- * @param {Node} element Target element
222
- * @param {string} pluginName The plugin name for the selected target.
223
- * @param {Object} [options] Options
224
- * @param {boolean} [options.isInput=false] Whether the target is an input component.(table)
225
- */
226
- select(element, pluginName, { isInput = false } = {}) {
227
- const info = this.get(element);
228
- if (!info || dom.check.isUneditable(dom.query.getParentElement(element, this.is.bind(this))) || dom.check.isUneditable(element)) return false;
229
-
230
- const plugin = info.launcher || this.plugins[pluginName];
231
- if (!plugin) return;
232
-
233
- if (!isInput && _DragHandle.get('__overInfo') !== ON_OVER_COMPONENT) {
234
- if (this.editor.status._onMousedown) return;
235
-
236
- this.editor._preventBlur = true;
237
- this.__selectionSelected = true;
238
- if (this.isInline(info.container)) {
239
- this.selection.setRange(info.container, 0, info.container, 0);
240
- }
241
- this.editor.blur();
242
- _w.setTimeout(() => {
243
- this.__selectionSelected = false;
244
- });
245
- }
246
-
247
- this.isSelected = true;
248
-
249
- let isNonFigureComponent;
250
- if (typeof plugin.select === 'function') isNonFigureComponent = plugin.select(element);
251
-
252
- const isBreakComponent = dom.utils.hasClass(info.target, 'se-component-line-break');
253
- if (isBreakComponent || (!isNonFigureComponent && !dom.utils.hasClass(info.container, 'se-inline-component'))) this._setComponentLineBreaker(/** @type {HTMLElement} */ (info.container || info.cover || element));
254
-
255
- this.currentTarget = element;
256
- this.currentPlugin = plugin;
257
- this.currentPluginName = pluginName;
258
- this.currentInfo = info;
259
-
260
- _DragHandle.set('__dragInst', this);
261
-
262
- const __overInfo = _DragHandle.get('__overInfo');
263
- _w.setTimeout(() => {
264
- _DragHandle.set('__overInfo', __overInfo === ON_OVER_COMPONENT ? undefined : false);
265
- if (__overInfo !== ON_OVER_COMPONENT) this.__addGlobalEvent();
266
- if (!info.isFile) this.__addNotFileGlobalEvent();
267
- }, 0);
268
-
269
- converter.debounce(() => {
270
- dom.utils.addClass(info.container, 'se-component-selected');
271
- }, 0)();
272
-
273
- if (!isBreakComponent && __overInfo !== ON_OVER_COMPONENT) {
274
- // set zero width space
275
- if (!this.isInline(info.container)) return;
276
-
277
- const oNode = info.container;
278
- let zeroWidth = null;
279
- if (!oNode.previousSibling || dom.check.isBreak(oNode.previousSibling)) {
280
- zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
281
- oNode.parentNode.insertBefore(zeroWidth, oNode);
282
- }
283
-
284
- if (!oNode.nextSibling || dom.check.isBreak(oNode.nextSibling)) {
285
- zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
286
- oNode.parentNode.insertBefore(zeroWidth, oNode.nextSibling);
287
- }
288
-
289
- this.editor.status.onSelected = true;
290
- } else if (isBreakComponent || !dom.utils.hasClass(info.container, 'se-input-component')) {
291
- const dragHandle = this.editor.frameContext.get('wrapper').querySelector('.se-drag-handle');
292
- dom.utils.addClass(dragHandle, 'se-drag-handle-full');
293
- this.ui._visibleControllers(false, false);
294
-
295
- const sizeTarget = info.caption ? info.target : info.cover || info.container || info.target;
296
- const w = sizeTarget.offsetWidth;
297
- const h = sizeTarget.offsetHeight;
298
- const { top, left } = this.offset.getLocal(sizeTarget);
299
-
300
- dragHandle.style.opacity = 0;
301
- dragHandle.style.width = w + 'px';
302
- dragHandle.style.height = h + 'px';
303
- dragHandle.style.top = top + 'px';
304
- dragHandle.style.left = left + 'px';
305
-
306
- _DragHandle.set('__dragHandler', dragHandle);
307
- _DragHandle.set('__dragContainer', info.container);
308
- _DragHandle.set('__dragCover', info.cover);
309
-
310
- dragHandle.style.display = 'block';
311
- }
312
- },
313
-
314
- /**
315
- * @this {ComponentThis}
316
- * @description Deselects the selected component.
317
- */
318
- deselect() {
319
- _w.setTimeout(() => {
320
- this.editor.status.onSelected = false;
321
- }, 0);
322
- this.__deselect();
323
- this.ui.setControllerOnDisabledButtons(false);
324
- },
325
-
326
- /**
327
- * @this {ComponentThis}
328
- * @description Determines if the specified node is a block component (e.g., img, iframe, video, audio, table) with the class "se-component"
329
- * - or a direct FIGURE node. This function checks if the node itself is a component
330
- * - or if it belongs to any components identified by the component manager.
331
- * @param {Node} element The DOM node to check.
332
- * @returns {boolean} True if the node is a block component or part of it, otherwise false.
333
- */
334
- is(element) {
335
- if (!element) return false;
336
-
337
- if (/^FIGURE$/i.test(element.nodeName) || dom.utils.hasClass(element, 'se-component')) return true;
338
- if (this.editor._componentManager.find((f) => f(element))) return true;
339
-
340
- return false;
341
- },
342
-
343
- /**
344
- * @this {ComponentThis}
345
- * @description Checks if the given node is an inline component (class "se-inline-component").
346
- * - If the node is a FIGURE, it checks the parent element instead.
347
- * - It also verifies whether the node is part of an inline component recognized by the component manager.
348
- * @param {Node} element The DOM node to check.
349
- * @returns {boolean} True if the node is an inline component or part of it, otherwise false.
350
- */
351
- isInline(element) {
352
- if (!element) return false;
353
-
354
- if (/^FIGURE$/i.test(element.nodeName)) element = element.parentElement;
355
- if (dom.utils.hasClass(element, 'se-inline-component')) return true;
356
-
357
- const container = this.editor._componentManager.find((f) => f(element));
358
- if (container && dom.utils.hasClass(element, 'se-inline-component')) return true;
359
-
360
- return false;
361
- },
362
-
363
- /**
364
- * @this {ComponentThis}
365
- * @description Checks if the specified node qualifies as a basic component within the editor.
366
- * - This function verifies whether the node is recognized as a component by the `is` function, while also ensuring that it is not an inline component as determined by the `isInline` function.
367
- * - This is used to identify block-level elements or standalone components that are not part of the inline component classification.
368
- * @param {Node} element The DOM node to check.
369
- * @returns {boolean} True if the node is a basic (non-inline) component, otherwise false.
370
- */
371
- isBasic(element) {
372
- return this.is(element) && !this.isInline(element);
373
- },
374
-
375
- /**
376
- * @this {ComponentThis}
377
- * @description Copies the specified component node to the clipboard.
378
- * - This function is different from the one called when the user presses the "Ctrl + C" key combination.
379
- * @param {Node} container The DOM node to check.
380
- */
381
- copy(container) {
382
- const cloneContainer = /** @type {HTMLElement} */ (dom.utils.clone(container, true));
383
-
384
- // remove selected class
385
- dom.utils.removeClass(cloneContainer, 'se-component-selected');
386
- dom.utils.removeClass(cloneContainer.querySelectorAll('.se-figure-selected'), 'se-figure-selected');
387
- dom.utils.removeClass(cloneContainer.querySelectorAll('.se-selected-table-cell'), 'se-selected-table-cell');
388
- dom.utils.removeClass(cloneContainer.querySelector('.se-selected-cell-focus'), 'se-selected-cell-focus');
389
-
390
- // copy to clipboard
391
- this.html.copy(cloneContainer);
392
-
393
- // copy effect
394
- dom.utils.flashClass(container, 'se-copy');
395
- },
396
-
397
- /**
398
- * @private
399
- * @this {ComponentThis}
400
- * @description Checks if the given element is a file component by matching its tag name against the file manager's regular expressions.
401
- * - It also verifies whether the element has the required attributes based on the tag type.
402
- * @param {Node} element The element to check.
403
- * @returns {boolean} Returns true if the element is a file component, otherwise false.
404
- */
405
- __isFiles(element) {
406
- const nodeName = element.nodeName.toLowerCase();
407
- return this.editor._fileManager.regExp.test(nodeName) && (!this.editor._fileManager.tagAttrs[nodeName] || this.editor._fileManager.tagAttrs[nodeName]?.every((v) => /** @type {HTMLElement} */ (element).hasAttribute(v)));
408
- },
409
-
410
- /**
411
- * @private
412
- * @this {ComponentThis}
413
- * @description Deselects the currently selected component, removing any selection effects and associated event listeners.
414
- * - This method resets the selection state and hides UI elements related to the component selection.
415
- */
416
- __deselect() {
417
- this.editor._preventBlur = false;
418
- _DragHandle.set('__overInfo', null);
419
- this._removeDragEvent();
420
-
421
- if (this.currentInfo) {
422
- const infoContainer = this.currentInfo.container;
423
- const infoCover = this.currentInfo.cover;
424
- converter.debounce(() => {
425
- dom.utils.removeClass(infoContainer, 'se-component-selected');
426
- dom.utils.removeClass(infoCover, 'se-figure-over-selected');
427
- }, 0)();
428
- }
429
-
430
- const { frameContext } = this.editor;
431
- frameContext.get('lineBreaker_t').style.display = frameContext.get('lineBreaker_b').style.display = 'none';
432
-
433
- if (this.currentPlugin && typeof this.currentPlugin.deselect === 'function') {
434
- this.currentPlugin.deselect(this.currentTarget);
435
- }
436
-
437
- this.isSelected = false;
438
- this.currentPlugin = null;
439
- this.currentTarget = null;
440
- this.currentPluginName = '';
441
- this.currentInfo = null;
442
- this.__removeGlobalEvent();
443
- this.ui.__offControllers();
444
- },
445
-
446
- /**
447
- * @private
448
- * @this {ComponentThis}
449
- * @description Set line breaker of component
450
- * @param {HTMLElement} element Element tag
451
- */
452
- _setComponentLineBreaker(element) {
453
- const _overInfo = _DragHandle.get('__overInfo') === ON_OVER_COMPONENT;
454
- this.eventManager._lineBreakComp = null;
455
- const info = this.get(element);
456
- if (!info) return;
457
-
458
- const fc = this.editor.frameContext;
459
- const container = info.container;
460
- const isNonSelected = dom.utils.hasClass(container, 'se-flex-component');
461
- const lb_t = fc.get('lineBreaker_t');
462
- const lb_b = fc.get('lineBreaker_b');
463
- const t_style = lb_t.style;
464
- const b_style = lb_b.style;
465
- const offsetTarget = container.offsetWidth < element.offsetWidth ? container : element;
466
- const target = this.editor._figureContainer?.style.display === 'block' ? this.editor._figureContainer : offsetTarget;
467
- const isList = dom.check.isListCell(container.parentNode);
468
-
469
- // top
470
- let componentTop, w;
471
- const isRtl = this.options.get('_rtl');
472
- const dir = isRtl ? ['right', 'left'] : ['left', 'right'];
473
- const { top, left, right, scrollX, scrollY } = this.offset.getLocal(offsetTarget);
474
- const sideOffset = isRtl ? right : left;
475
-
476
- if (isList ? !container.previousSibling : !this.format.isLine(container.previousElementSibling)) {
477
- const cStyle = _w.getComputedStyle(lb_t);
478
- const cH = numbers.get(cStyle.height, 1);
479
- const cW = numbers.get(cStyle.width, 1);
480
-
481
- this.eventManager._lineBreakComp = container;
482
- componentTop = top;
483
- w = target.offsetWidth / 2 / 2;
484
-
485
- t_style.top = componentTop - scrollY - cH / 2 + 'px';
486
- t_style[dir[0]] = (isNonSelected ? sideOffset - cW / 2 : sideOffset + w) + 'px';
487
- t_style[dir[1]] = '';
488
-
489
- lb_t.setAttribute('data-offset', scrollY + ',' + scrollX);
490
- if (_overInfo) dom.utils.removeClass(lb_t, 'se-on-selected');
491
- else dom.utils.addClass(lb_t, 'se-on-selected');
492
-
493
- t_style.display = 'block';
494
- t_style.visibility = '';
495
- } else {
496
- t_style.display = 'none';
497
- }
498
-
499
- // bottom
500
- if (isList ? !container.nextSibling : !this.format.isLine(container.nextElementSibling)) {
501
- const cStyle = _w.getComputedStyle(lb_b);
502
- const cH = numbers.get(cStyle.height, 1);
503
- const cW = numbers.get(cStyle.width, 1);
504
-
505
- if (!componentTop) {
506
- this.eventManager._lineBreakComp = container;
507
- componentTop = top;
508
- w = target.offsetWidth / 2 / 2;
509
- }
510
-
511
- b_style.top = componentTop + target.offsetHeight - scrollY - cH / 2 + 'px';
512
- b_style[dir[0]] = sideOffset + target.offsetWidth - (isNonSelected ? 0 : w) - (isNonSelected ? cW / 2 : cW) + 'px';
513
- b_style[dir[1]] = '';
514
-
515
- lb_b.setAttribute('data-offset', scrollY + ',' + dir[0] + ',' + scrollX);
516
- if (_overInfo) dom.utils.removeClass(lb_b, 'se-on-selected');
517
- else dom.utils.addClass(lb_b, 'se-on-selected');
518
-
519
- b_style.display = 'block';
520
- b_style.visibility = '';
521
- } else {
522
- b_style.display = 'none';
523
- }
524
- },
525
-
526
- /**
527
- * @private
528
- * @this {ComponentThis}
529
- * @description Adds global event listeners for component interactions such as copy, cut, and keydown events.
530
- */
531
- __addGlobalEvent() {
532
- this.__removeGlobalEvent();
533
- this._bindClose_copy = this.eventManager.addGlobalEvent('copy', this.__globalEvents.copy);
534
- this._bindClose_cut = this.eventManager.addGlobalEvent('cut', this.__globalEvents.cut);
535
- this._bindClose_keydown = this.eventManager.addGlobalEvent('keydown', this.__globalEvents.keydown);
536
- },
537
-
538
- /**
539
- * @private
540
- * @this {ComponentThis}
541
- * @description Removes global event listeners that were previously added for component interactions.
542
- */
543
- __removeGlobalEvent() {
544
- this.__removeNotFileGlobalEvent();
545
- if (this._bindClose_copy) this._bindClose_copy = this.eventManager.removeGlobalEvent(this._bindClose_copy);
546
- if (this._bindClose_cut) this._bindClose_cut = this.eventManager.removeGlobalEvent(this._bindClose_cut);
547
- if (this._bindClose_keydown) this._bindClose_keydown = this.eventManager.removeGlobalEvent(this._bindClose_keydown);
548
- },
549
-
550
- /**
551
- * @private
552
- * @this {ComponentThis}
553
- * @description Adds global event listeners for non-file-related interactions such as mouse and touch events.
554
- */
555
- __addNotFileGlobalEvent() {
556
- this.__removeNotFileGlobalEvent();
557
- if (!isMobile) this._bindClose_mousedown = this.eventManager.addGlobalEvent('mousedown', this.__globalEvents.mousedown, true);
558
- else this._bindClose_touchstart = this.eventManager.addGlobalEvent('touchstart', this.__globalEvents.mousedown, true);
559
- },
560
-
561
- /**
562
- * @private
563
- * @this {ComponentThis}
564
- * @description Removes global event listeners related to non-file interactions.
565
- */
566
- __removeNotFileGlobalEvent() {
567
- if (this._bindClose_mousedown) this._bindClose_mousedown = this.eventManager.removeGlobalEvent(this._bindClose_mousedown);
568
- if (this._bindClose_touchstart) this._bindClose_touchstart = this.eventManager.removeGlobalEvent(this._bindClose_touchstart);
569
- },
570
-
571
- /**
572
- * @private
573
- * @this {ComponentThis}
574
- * @description Removes drag-related events and resets drag-related states.
575
- */
576
- _removeDragEvent() {
577
- /** @type {HTMLElement} */ (this.carrierWrapper.querySelector('.se-drag-cursor')).style.left = '-10000px';
578
- if (_DragHandle.get('__dragHandler')) _DragHandle.get('__dragHandler').style.display = 'none';
579
-
580
- dom.utils.removeClass([_DragHandle.get('__dragHandler'), _DragHandle.get('__dragContainer')], 'se-dragging');
581
- dom.utils.removeClass([_DragHandle.get('__dragCover'), _DragHandle.get('__dragContainer')], 'se-drag-over');
582
-
583
- _DragHandle.set('__figureInst', null);
584
- _DragHandle.set('__dragInst', null);
585
- _DragHandle.set('__dragHandler', null);
586
- _DragHandle.set('__dragContainer', null);
587
- _DragHandle.set('__dragCover', null);
588
- _DragHandle.set('__dragMove', null);
589
- _DragHandle.set('__overInfo', null);
590
- },
591
-
592
- constructor: Component
593
- };
594
-
595
- /**
596
- * @this {ComponentThis}
597
- */
598
- function OnDragEnter() {
599
- this.editor._preventBlur = true;
600
- this.ui._visibleControllers(false, dom.utils.hasClass(_DragHandle.get('__dragHandler'), 'se-drag-handle-full'));
601
- dom.utils.addClass(_DragHandle.get('__dragCover') || _DragHandle.get('__dragContainer'), 'se-drag-over');
602
- }
603
-
604
- /**
605
- * @this {ComponentThis}
606
- */
607
- function OnDragLeave() {
608
- this.editor._preventBlur = false;
609
- if (!dom.utils.hasClass(_DragHandle.get('__dragHandler'), 'se-drag-handle-full')) this.ui._visibleControllers(true, true);
610
- dom.utils.removeClass([_DragHandle.get('__dragCover'), _DragHandle.get('__dragContainer')], 'se-drag-over');
611
- }
612
-
613
- /**
614
- * @this {ComponentThis}
615
- * @param {DragEvent} e - Drag event
616
- */
617
- function OnDragStart(e) {
618
- const cover = _DragHandle.get('__dragCover') || _DragHandle.get('__dragContainer');
619
-
620
- if (!cover) {
621
- e.preventDefault();
622
- return;
623
- }
624
-
625
- this.editor._preventBlur = false;
626
- dom.utils.addClass(_DragHandle.get('__dragHandler'), 'se-dragging');
627
- dom.utils.addClass(_DragHandle.get('__dragContainer'), 'se-dragging');
628
- e.dataTransfer.setDragImage(cover, this.options.get('_rtl') ? cover.offsetWidth : -5, -5);
629
- }
630
-
631
- /**
632
- * @this {ComponentThis}
633
- */
634
- function OnDragEnd() {
635
- this.editor._preventBlur = false;
636
- dom.utils.removeClass([_DragHandle.get('__dragHandler'), _DragHandle.get('__dragContainer')], 'se-dragging');
637
- this._removeDragEvent();
638
- }
639
-
640
- /**
641
- * @this {ComponentThis}
642
- * @param {MouseEvent} e - Mouse event
643
- */
644
- function OnDragClick(e) {
645
- const target = dom.query.getEventTarget(e);
646
- if (!dom.utils.hasClass(target, 'se-drag-handle-full')) return;
647
-
648
- const dragInst = _DragHandle.get('__dragInst');
649
- this._removeDragEvent();
650
- this.select(dragInst.currentTarget, dragInst.currentPluginName);
651
- }
652
-
653
- /**
654
- * @this {ComponentThis}
655
- * @param {MouseEvent} e - Mouse event
656
- */
657
- function CloseListener_mousedown(e) {
658
- const target = dom.query.getEventTarget(e);
659
- if (
660
- this.currentTarget?.contains(target) ||
661
- dom.query.getParentElement(target, '.se-controller') ||
662
- dom.utils.hasClass(target, 'se-drag-handle') ||
663
- (this.currentPluginName === this.editor.currentControllerName && this.editor.opendControllers.some(({ form }) => form.contains(target)))
664
- ) {
665
- return;
666
- }
667
- this.deselect();
668
- }
669
-
670
- /**
671
- * @this {ComponentThis}
672
- * @param {ClipboardEvent} e - Event object
673
- */
674
- function OnCopy_component(e) {
675
- const target = dom.query.getEventTarget(e);
676
- if (dom.check.isInputElement(target) && dom.query.getParentElement(target, '.se-modal')) return;
677
-
678
- const info = this.info;
679
- if (!info) return;
680
-
681
- const cloneContainer = info.container.cloneNode(true);
682
- dom.utils.removeClass(cloneContainer, 'se-component-selected');
683
-
684
- if (typeof this.plugins[info.pluginName]?.onCopyComponent !== 'function' || this.plugins[info.pluginName].onCopyComponent({ event: e, cloneContainer, info }) === false) {
685
- SetClipboardComponent(e, cloneContainer, e.clipboardData);
686
- }
687
-
688
- // copy effect
689
- dom.utils.flashClass(info.container, 'se-copy');
690
- }
691
-
692
- /**
693
- * @this {ComponentThis}
694
- * @param {ClipboardEvent} e - Event object
695
- */
696
- function OnCut_component(e) {
697
- const info = this.info;
698
- if (!info) return;
699
-
700
- const cloneContainer = info.container.cloneNode(true);
701
- dom.utils.removeClass(cloneContainer, 'se-component-selected');
702
-
703
- SetClipboardComponent(e, cloneContainer, e.clipboardData);
704
- this.deselect();
705
- dom.utils.removeItem(info.container);
706
- }
707
-
708
- /**
709
- * @this {ComponentThis}
710
- * @param {KeyboardEvent} e - Event object
711
- */
712
- async function OnKeyDown_component(e) {
713
- if (this.editor.selectMenuOn) return;
714
-
715
- const keyCode = e.code;
716
- const ctrl = keyCodeMap.isCtrl(e);
717
-
718
- // redo, undo
719
- if (ctrl) {
720
- if (keyCode !== 'ControlRight' && keyCode !== 'ControlLeft') {
721
- const info = this.editor.shortcutsKeyMap.get(keyCode + (e.shiftKey ? '1000' : ''));
722
- if (/^(redo|undo)$/.test(info?.c)) {
723
- e.preventDefault();
724
- e.stopPropagation();
725
- this.editor.run(info.c, info.t, info.e);
726
- }
727
- }
728
- return;
729
- }
730
-
731
- // backspace key, delete key
732
- if (keyCodeMap.isRemoveKey(keyCode)) {
733
- e.preventDefault();
734
- e.stopPropagation();
735
- if (typeof this.currentPlugin?.destroy === 'function') {
736
- await this.currentPlugin.destroy(this.currentTarget);
737
- this.deselect();
738
- this.editor.focus();
739
- return;
740
- }
741
- }
742
-
743
- // enter key
744
- if (keyCodeMap.isEnter(keyCode)) {
745
- e.preventDefault();
746
- const compContext = this.currentInfo || this.get(this.currentTarget);
747
- const container = compContext.container || compContext.target;
748
- const sibling = container.previousElementSibling || container.nextElementSibling;
749
- let newEl = null;
750
- if (dom.check.isListCell(container.parentNode)) {
751
- newEl = dom.utils.createElement('BR');
752
- } else {
753
- newEl = dom.utils.createElement(this.format.isLine(sibling) ? sibling.nodeName : this.options.get('defaultLine'), null, '<br>');
754
- }
755
-
756
- const pluginName = this.currentPluginName;
757
- this.deselect();
758
- container.parentNode.insertBefore(newEl, container);
759
- if (this.select(compContext.target, pluginName) === false) this.editor.blur();
760
- this.history.push(false);
761
-
762
- return;
763
- }
764
-
765
- // up down, left right
766
- if (DIR_KEYCODE.test(keyCode)) {
767
- const { container } = this.get(this.currentTarget);
768
- const isInline = this.isInline(container || this.currentTarget);
769
-
770
- let el = null;
771
- let offset = 1;
772
- if (isInline) {
773
- switch (keyCode) {
774
- case 'ArrowLeft': // left
775
- el = container.previousSibling;
776
- offset = el?.nodeType === 3 ? el.textContent.length : 1;
777
- break;
778
- case 'ArrowRight': // right
779
- el = container.nextSibling;
780
- offset = 0;
781
- break;
782
- case 'ArrowUp': {
783
- // up
784
- const line = this.format.getLine(container, null);
785
- el = line?.previousElementSibling;
786
- offset = 0;
787
- break;
788
- }
789
- case 'ArrowDown': {
790
- // down
791
- const line = this.format.getLine(container, null);
792
- el = line?.nextElementSibling;
793
- break;
794
- }
795
- }
796
- } else {
797
- if (DIR_UP_KEYCODE.test(keyCode)) {
798
- el = container.previousElementSibling;
799
- } else {
800
- el = container.nextElementSibling;
801
- offset = 0;
802
- }
803
- }
804
-
805
- if (!el) return;
806
-
807
- this.deselect();
808
-
809
- const elComp = this.get(el);
810
- if (elComp?.container) {
811
- e.stopPropagation();
812
- e.preventDefault();
813
- this.select(elComp.target, elComp.pluginName);
814
- } else {
815
- try {
816
- this.editor._preventBlur = true;
817
- e.stopPropagation();
818
- e.preventDefault();
819
- this.selection.setRange(el, offset, el, offset);
820
- } finally {
821
- this.editor._preventBlur = false;
822
- }
823
- }
824
-
825
- return;
826
- }
827
-
828
- // ESC
829
- if (keyCodeMap.isEsc(keyCode)) {
830
- this.deselect();
831
- return;
832
- }
833
- }
834
-
835
- function SetClipboardComponent(e, container, clipboardData) {
836
- e.preventDefault();
837
- e.stopPropagation();
838
- container.querySelectorAll('.se-figure-selected').forEach((el) => dom.utils.removeClass(el, 'se-figure-selected'));
839
- clipboardData.setData('text/html', container.outerHTML);
840
- }
841
-
842
- export default Component;
1
+ /**
2
+ * @fileoverview Component class
3
+ */
4
+
5
+ import CoreInjector from '../../editorInjector/_core';
6
+ import { dom, env, numbers, unicode, keyCodeMap, converter } from '../../helper';
7
+ import { Figure, _DragHandle } from '../../modules';
8
+
9
+ const { _w, ON_OVER_COMPONENT, isMobile } = env;
10
+ const DIR_KEYCODE = /^Arrow(Left|Up|Right|Down)$/;
11
+ const DIR_UP_KEYCODE = /^Arrow(Left|Up)$/;
12
+
13
+ /**
14
+ * @typedef {Omit<Component & Partial<__se__EditorInjector>, 'component'>} ComponentThis
15
+ */
16
+
17
+ /**
18
+ * @constructor
19
+ * @this {ComponentThis}
20
+ * @description Class for managing components such as images and tables that are not in line format
21
+ * @param {__se__EditorCore} editor - The root editor instance
22
+ */
23
+ function Component(editor) {
24
+ CoreInjector.call(this, editor);
25
+
26
+ /**
27
+ * @description The current component information, used copy, cut, and keydown events
28
+ * @type {__se__ComponentInfo}
29
+ */
30
+ this.info = null;
31
+
32
+ /**
33
+ * @description Component is selected
34
+ * @type {boolean}
35
+ */
36
+ this.isSelected = false;
37
+
38
+ /**
39
+ * @description Currently selected component target
40
+ * @type {Node|null}
41
+ */
42
+ this.currentTarget = null;
43
+
44
+ /**
45
+ * @description Currently selected component plugin instance
46
+ * @type {*}
47
+ */
48
+ this.currentPlugin = null;
49
+
50
+ /**
51
+ * @description Currently selected component plugin name
52
+ * @type {*}
53
+ */
54
+ this.currentPluginName = '';
55
+
56
+ /**
57
+ * @description Currently selected component information
58
+ * @type {__se__ComponentInfo|null}
59
+ */
60
+ this.currentInfo = null;
61
+
62
+ /** @type {Object<string, (...args: *) => *>} */
63
+ this.__globalEvents = {
64
+ copy: OnCopy_component.bind(this),
65
+ cut: OnCut_component.bind(this),
66
+ keydown: OnKeyDown_component.bind(this),
67
+ mousedown: CloseListener_mousedown.bind(this)
68
+ };
69
+ /** @type {__se__GlobalEventInfo|void} */
70
+ this._bindClose_copy = null;
71
+ /** @type {__se__GlobalEventInfo|void} */
72
+ this._bindClose_cut = null;
73
+ /** @type {__se__GlobalEventInfo|void} */
74
+ this._bindClose_keydown = null;
75
+ /** @type {__se__GlobalEventInfo|void} */
76
+ this._bindClose_mousedown = null;
77
+ /** @type {boolean} */
78
+ this.__selectionSelected = false;
79
+
80
+ this.__prevent = false;
81
+
82
+ this.editor.applyFrameRoots((e) => {
83
+ // drag
84
+ const dragHandle = dom.utils.createElement('DIV', { class: 'se-drag-handle', draggable: 'true' }, this.icons.selection);
85
+ e.get('wrapper').appendChild(dragHandle);
86
+ this.eventManager.addEvent(dragHandle, 'mouseenter', OnDragEnter.bind(this));
87
+ this.eventManager.addEvent(dragHandle, 'mouseleave', OnDragLeave.bind(this));
88
+ this.eventManager.addEvent(dragHandle, 'dragstart', OnDragStart.bind(this));
89
+ this.eventManager.addEvent(dragHandle, 'dragend', OnDragEnd.bind(this));
90
+ this.eventManager.addEvent(dragHandle, 'click', OnDragClick.bind(this));
91
+ });
92
+ }
93
+
94
+ Component.prototype = {
95
+ /**
96
+ * @this {ComponentThis}
97
+ * @description Inserts an element and returns it. (Used for elements: table, hr, image, video)
98
+ * - If "element" is "HR", inserts and returns the new line.
99
+ * @param {Node} element Element to be inserted
100
+ * @param {Object} [options] Options
101
+ * @param {boolean} [options.skipCharCount=false] If true, it will be inserted even if "frameOptions.get('charCounter_max')" is exceeded.
102
+ * @param {boolean} [options.skipSelection=false] If true, do not automatically select the inserted component.
103
+ * @param {boolean} [options.skipHistory=false] If true, do not push to history.
104
+ * @returns {HTMLElement} The inserted element or new line (for HR)
105
+ */
106
+ insert(element, { skipCharCount, skipSelection, skipHistory } = {}) {
107
+ if (this.editor.frameContext.get('isReadOnly') || (!skipCharCount && !this.char.check(element))) {
108
+ return null;
109
+ }
110
+
111
+ const r = this.html.remove();
112
+ const isInline = this.isInline(element);
113
+ this.selection.getRangeAndAddLine(this.selection.getRange(), r.container);
114
+ const selectionNode = this.selection.getNode();
115
+ let oNode = null;
116
+ let formatEl = this.format.getLine(selectionNode, null);
117
+
118
+ if (dom.check.isListCell(formatEl)) {
119
+ this.html.insertNode(element, { afterNode: isInline ? null : selectionNode === formatEl ? null : r.container.nextSibling, skipCharCount: true });
120
+ if (!isInline && !element.nextSibling) element.parentNode.appendChild(dom.utils.createElement('BR'));
121
+ } else {
122
+ if (!isInline && this.selection.getRange().collapsed && (r.container.nodeType === 3 || dom.check.isBreak(r.container))) {
123
+ const depthFormat = dom.query.getParentElement(r.container, this.format.isBlock.bind(this.format));
124
+ oNode = this.nodeTransform.split(r.container, r.offset, !depthFormat ? 0 : dom.query.getNodeDepth(depthFormat) + 1);
125
+ if (oNode) formatEl = /** @type {HTMLElement} */ (oNode.previousSibling);
126
+ }
127
+ this.html.insertNode(element, { afterNode: isInline ? null : this.format.isBlock(formatEl) ? null : formatEl, skipCharCount: true });
128
+ if (!isInline && formatEl && dom.check.isZeroWidth(formatEl)) dom.utils.removeItem(formatEl);
129
+ }
130
+
131
+ if (isInline) {
132
+ const empty = dom.utils.createTextNode(unicode.zeroWidthSpace);
133
+ element.parentNode.insertBefore(empty, element.nextSibling);
134
+ }
135
+
136
+ if (!skipHistory) this.history.push(false);
137
+
138
+ if (!skipSelection) {
139
+ this.selection.setRange(element, 0, element, 0);
140
+ const fileComponentInfo = this.get(element);
141
+ if (fileComponentInfo) {
142
+ this.select(fileComponentInfo.target, fileComponentInfo.pluginName);
143
+ } else if (oNode) {
144
+ oNode = dom.query.getEdgeChildNodes(oNode, null).sc || oNode;
145
+ this.selection.setRange(oNode, 0, oNode, 0);
146
+ }
147
+ }
148
+
149
+ // document type
150
+ if (this.editor.frameContext.has('documentType-use-header')) {
151
+ this.editor.frameContext.get('documentType').reHeader();
152
+ }
153
+
154
+ return /** @type {HTMLElement} */ (oNode || element);
155
+ },
156
+
157
+ /**
158
+ * @this {ComponentThis}
159
+ * @description Gets the file component and that plugin name
160
+ * - return: {target, component, pluginName} | null
161
+ * @param {Node} element Target element (figure tag, component div, file tag)
162
+ * @returns {__se__ComponentInfo|null}
163
+ */
164
+ get(element) {
165
+ if (!element) return null;
166
+
167
+ let target;
168
+ let pluginName = '';
169
+ let options = {};
170
+ let isFile = false;
171
+ let launcher = null;
172
+
173
+ if (this.is(element)) {
174
+ if (dom.utils.hasClass(element, 'se-component') && !dom.utils.hasClass(element, 'se-inline-component')) element = /** @type {HTMLElement} */ (element).firstElementChild || element;
175
+ if (/^FIGURE$/i.test(element.nodeName)) element = /** @type {HTMLElement} */ (element).firstElementChild;
176
+ if (!element) return null;
177
+
178
+ const comp = this.editor._componentManager.map((f) => f(element)).find((e) => e);
179
+ if (!comp) return null;
180
+ target = comp.target;
181
+ pluginName = comp.pluginName;
182
+ options = comp.options;
183
+ launcher = comp.launcher;
184
+ }
185
+
186
+ if (!target && element.nodeName) {
187
+ if (this.__isFiles(element)) {
188
+ isFile = true;
189
+ }
190
+ const comp = this.editor._componentManager.map((f) => f(element)).find((e) => e);
191
+ if (!comp) return null;
192
+ target = comp.target;
193
+ pluginName = comp.pluginName;
194
+ options = comp.options;
195
+ launcher = comp.launcher;
196
+ }
197
+
198
+ if (!target) {
199
+ return null;
200
+ }
201
+
202
+ const figureInfo = Figure.GetContainer(target);
203
+ const container = figureInfo.container || figureInfo.cover || target;
204
+ return (this.info = {
205
+ target,
206
+ pluginName,
207
+ options,
208
+ container: container,
209
+ cover: figureInfo.cover,
210
+ inlineCover: figureInfo.inlineCover,
211
+ caption: figureInfo.caption,
212
+ isFile,
213
+ launcher,
214
+ isInputType: dom.utils.hasClass(container, 'se-input-component')
215
+ });
216
+ },
217
+
218
+ /**
219
+ * @this {ComponentThis}
220
+ * @description The component(media, file component, table, etc) is selected and the resizing module is called.
221
+ * @param {Node} element Target element
222
+ * @param {string} pluginName The plugin name for the selected target.
223
+ * @param {Object} [options] Options
224
+ * @param {boolean} [options.isInput=false] Whether the target is an input component.(table)
225
+ */
226
+ select(element, pluginName, { isInput = false } = {}) {
227
+ const info = this.get(element);
228
+ if (!info || dom.check.isUneditable(dom.query.getParentElement(element, this.is.bind(this))) || dom.check.isUneditable(element)) return false;
229
+
230
+ const plugin = info.launcher || this.plugins[pluginName];
231
+ if (!plugin) return;
232
+
233
+ const notOver = _DragHandle.get('__overInfo') !== ON_OVER_COMPONENT;
234
+ if (!isInput && notOver) {
235
+ if (this.editor.status._onMousedown) return;
236
+
237
+ this.editor._preventBlur = true;
238
+ this.__selectionSelected = true;
239
+ if (this.isInline(info.container)) {
240
+ this.selection.setRange(info.container, 0, info.container, 0);
241
+ }
242
+ this.editor.blur();
243
+ _w.setTimeout(() => {
244
+ this.__selectionSelected = false;
245
+ });
246
+ }
247
+
248
+ this.isSelected = true;
249
+ this.__prevent = true;
250
+
251
+ let isNonFigureComponent;
252
+ if (typeof plugin.select === 'function') isNonFigureComponent = plugin.select(element);
253
+
254
+ const isBreakComponent = dom.utils.hasClass(info.target, 'se-component-line-break');
255
+ if (isBreakComponent || (!isNonFigureComponent && !dom.utils.hasClass(info.container, 'se-inline-component'))) this._setComponentLineBreaker(/** @type {HTMLElement} */ (info.container || info.cover || element));
256
+
257
+ this.currentTarget = element;
258
+ this.currentPlugin = plugin;
259
+ this.currentPluginName = pluginName;
260
+ this.currentInfo = info;
261
+
262
+ _DragHandle.set('__dragInst', this);
263
+
264
+ const __overInfo = _DragHandle.get('__overInfo');
265
+ _w.setTimeout(() => {
266
+ _DragHandle.set('__overInfo', __overInfo === ON_OVER_COMPONENT ? undefined : false);
267
+ if (__overInfo !== ON_OVER_COMPONENT) this.__addGlobalEvent();
268
+ if (!info.isFile) this.__addNotFileGlobalEvent();
269
+ }, 0);
270
+
271
+ converter.debounce(() => {
272
+ dom.utils.addClass(info.container, 'se-component-selected');
273
+ }, 0)();
274
+
275
+ if (notOver && !this.status.hasFocus && !this.editor._preventFocus) {
276
+ this.eventManager.__postFocusEvent(this.editor.frameContext, null);
277
+ this.editor._preventFocus = true;
278
+ }
279
+
280
+ if (!isBreakComponent && __overInfo !== ON_OVER_COMPONENT) {
281
+ // set zero width space
282
+ if (!this.isInline(info.container)) return;
283
+
284
+ const oNode = info.container;
285
+ let zeroWidth = null;
286
+ if (!oNode.previousSibling || dom.check.isBreak(oNode.previousSibling)) {
287
+ zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
288
+ oNode.parentNode.insertBefore(zeroWidth, oNode);
289
+ }
290
+
291
+ if (!oNode.nextSibling || dom.check.isBreak(oNode.nextSibling)) {
292
+ zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
293
+ oNode.parentNode.insertBefore(zeroWidth, oNode.nextSibling);
294
+ }
295
+
296
+ this.editor.status.onSelected = true;
297
+ } else if (isBreakComponent || !dom.utils.hasClass(info.container, 'se-input-component')) {
298
+ const dragHandle = this.editor.frameContext.get('wrapper').querySelector('.se-drag-handle');
299
+ dom.utils.addClass(dragHandle, 'se-drag-handle-full');
300
+ this.ui._visibleControllers(false, false);
301
+
302
+ const sizeTarget = info.caption ? info.target : info.cover || info.container || info.target;
303
+ const w = sizeTarget.offsetWidth;
304
+ const h = sizeTarget.offsetHeight;
305
+ const { top, left } = this.offset.getLocal(sizeTarget);
306
+
307
+ dragHandle.style.opacity = 0;
308
+ dragHandle.style.width = w + 'px';
309
+ dragHandle.style.height = h + 'px';
310
+ dragHandle.style.top = top + 'px';
311
+ dragHandle.style.left = left + 'px';
312
+
313
+ _DragHandle.set('__dragHandler', dragHandle);
314
+ _DragHandle.set('__dragContainer', info.container);
315
+ _DragHandle.set('__dragCover', info.cover);
316
+
317
+ dragHandle.style.display = 'block';
318
+ }
319
+ },
320
+
321
+ /**
322
+ * @this {ComponentThis}
323
+ * @description Deselects the selected component.
324
+ */
325
+ deselect() {
326
+ _w.setTimeout(() => {
327
+ this.editor.status.onSelected = false;
328
+ }, 0);
329
+ this.__deselect();
330
+ this.ui.setControllerOnDisabledButtons(false);
331
+
332
+ if (this.editor._preventFocus && !this.status.hasFocus && !this.__prevent) {
333
+ this.eventManager.__postBlurEvent(this.editor.frameContext, null);
334
+ this.editor._preventFocus = false;
335
+ }
336
+ },
337
+
338
+ /**
339
+ * @this {ComponentThis}
340
+ * @description Determines if the specified node is a block component (e.g., img, iframe, video, audio, table) with the class "se-component"
341
+ * - or a direct FIGURE node. This function checks if the node itself is a component
342
+ * - or if it belongs to any components identified by the component manager.
343
+ * @param {Node} element The DOM node to check.
344
+ * @returns {boolean} True if the node is a block component or part of it, otherwise false.
345
+ */
346
+ is(element) {
347
+ if (!element) return false;
348
+
349
+ if (/^FIGURE$/i.test(element.nodeName) || dom.utils.hasClass(element, 'se-component')) return true;
350
+ if (this.editor._componentManager.find((f) => f(element))) return true;
351
+
352
+ return false;
353
+ },
354
+
355
+ /**
356
+ * @this {ComponentThis}
357
+ * @description Checks if the given node is an inline component (class "se-inline-component").
358
+ * - If the node is a FIGURE, it checks the parent element instead.
359
+ * - It also verifies whether the node is part of an inline component recognized by the component manager.
360
+ * @param {Node} element The DOM node to check.
361
+ * @returns {boolean} True if the node is an inline component or part of it, otherwise false.
362
+ */
363
+ isInline(element) {
364
+ if (!element) return false;
365
+
366
+ if (/^FIGURE$/i.test(element.nodeName)) element = element.parentElement;
367
+ if (dom.utils.hasClass(element, 'se-inline-component')) return true;
368
+
369
+ const container = this.editor._componentManager.find((f) => f(element));
370
+ if (container && dom.utils.hasClass(element, 'se-inline-component')) return true;
371
+
372
+ return false;
373
+ },
374
+
375
+ /**
376
+ * @this {ComponentThis}
377
+ * @description Checks if the specified node qualifies as a basic component within the editor.
378
+ * - This function verifies whether the node is recognized as a component by the `is` function, while also ensuring that it is not an inline component as determined by the `isInline` function.
379
+ * - This is used to identify block-level elements or standalone components that are not part of the inline component classification.
380
+ * @param {Node} element The DOM node to check.
381
+ * @returns {boolean} True if the node is a basic (non-inline) component, otherwise false.
382
+ */
383
+ isBasic(element) {
384
+ return this.is(element) && !this.isInline(element);
385
+ },
386
+
387
+ /**
388
+ * @this {ComponentThis}
389
+ * @description Copies the specified component node to the clipboard.
390
+ * - This function is different from the one called when the user presses the "Ctrl + C" key combination.
391
+ * @param {Node} container The DOM node to check.
392
+ */
393
+ copy(container) {
394
+ const cloneContainer = /** @type {HTMLElement} */ (dom.utils.clone(container, true));
395
+
396
+ // remove selected class
397
+ dom.utils.removeClass(cloneContainer, 'se-component-selected');
398
+ dom.utils.removeClass(cloneContainer.querySelectorAll('.se-figure-selected'), 'se-figure-selected');
399
+ dom.utils.removeClass(cloneContainer.querySelectorAll('.se-selected-table-cell'), 'se-selected-table-cell');
400
+ dom.utils.removeClass(cloneContainer.querySelector('.se-selected-cell-focus'), 'se-selected-cell-focus');
401
+
402
+ // copy to clipboard
403
+ this.html.copy(cloneContainer);
404
+
405
+ // copy effect
406
+ dom.utils.flashClass(container, 'se-copy');
407
+ },
408
+
409
+ /**
410
+ * @private
411
+ * @this {ComponentThis}
412
+ * @description Checks if the given element is a file component by matching its tag name against the file manager's regular expressions.
413
+ * - It also verifies whether the element has the required attributes based on the tag type.
414
+ * @param {Node} element The element to check.
415
+ * @returns {boolean} Returns true if the element is a file component, otherwise false.
416
+ */
417
+ __isFiles(element) {
418
+ const nodeName = element.nodeName.toLowerCase();
419
+ return this.editor._fileManager.regExp.test(nodeName) && (!this.editor._fileManager.tagAttrs[nodeName] || this.editor._fileManager.tagAttrs[nodeName]?.every((v) => /** @type {HTMLElement} */ (element).hasAttribute(v)));
420
+ },
421
+
422
+ /**
423
+ * @private
424
+ * @this {ComponentThis}
425
+ * @description Deselects the currently selected component, removing any selection effects and associated event listeners.
426
+ * - This method resets the selection state and hides UI elements related to the component selection.
427
+ */
428
+ __deselect() {
429
+ this.editor._preventBlur = false;
430
+ _DragHandle.set('__overInfo', null);
431
+ this._removeDragEvent();
432
+
433
+ if (this.currentInfo) {
434
+ const infoContainer = this.currentInfo.container;
435
+ const infoCover = this.currentInfo.cover;
436
+ converter.debounce(() => {
437
+ dom.utils.removeClass(infoContainer, 'se-component-selected');
438
+ dom.utils.removeClass(infoCover, 'se-figure-over-selected');
439
+ }, 0)();
440
+ }
441
+
442
+ const { frameContext } = this.editor;
443
+ if (frameContext.get('lineBreaker_t')) {
444
+ frameContext.get('lineBreaker_t').style.display = frameContext.get('lineBreaker_b').style.display = 'none';
445
+ }
446
+
447
+ if (this.currentPlugin && typeof this.currentPlugin.deselect === 'function') {
448
+ this.currentPlugin.deselect(this.currentTarget);
449
+ }
450
+
451
+ this.isSelected = false;
452
+ this.currentPlugin = null;
453
+ this.currentTarget = null;
454
+ this.currentPluginName = '';
455
+ this.currentInfo = null;
456
+ this.__removeGlobalEvent();
457
+ this.ui.__offControllers();
458
+ },
459
+
460
+ /**
461
+ * @private
462
+ * @this {ComponentThis}
463
+ * @description Set line breaker of component
464
+ * @param {HTMLElement} element Element tag
465
+ */
466
+ _setComponentLineBreaker(element) {
467
+ const _overInfo = _DragHandle.get('__overInfo') === ON_OVER_COMPONENT;
468
+ this.eventManager._lineBreakComp = null;
469
+ const info = this.get(element);
470
+ if (!info) return;
471
+
472
+ const fc = this.editor.frameContext;
473
+ const container = info.container;
474
+ const isNonSelected = dom.utils.hasClass(container, 'se-flex-component');
475
+ const lb_t = fc.get('lineBreaker_t');
476
+ const lb_b = fc.get('lineBreaker_b');
477
+ const t_style = lb_t.style;
478
+ const b_style = lb_b.style;
479
+ const offsetTarget = container.offsetWidth < element.offsetWidth ? container : element;
480
+ const target = this.editor._figureContainer?.style.display === 'block' ? this.editor._figureContainer : offsetTarget;
481
+ const isList = dom.check.isListCell(container.parentNode);
482
+
483
+ // top
484
+ let componentTop, w;
485
+ const isRtl = this.options.get('_rtl');
486
+ const dir = isRtl ? ['right', 'left'] : ['left', 'right'];
487
+ const { top, left, right, scrollX, scrollY } = this.offset.getLocal(offsetTarget);
488
+ const sideOffset = isRtl ? right : left;
489
+
490
+ if (isList ? !container.previousSibling : !this.format.isLine(container.previousElementSibling)) {
491
+ const cStyle = _w.getComputedStyle(lb_t);
492
+ const cH = numbers.get(cStyle.height, 1);
493
+ const cW = numbers.get(cStyle.width, 1);
494
+
495
+ this.eventManager._lineBreakComp = container;
496
+ componentTop = top;
497
+ w = target.offsetWidth / 2 / 2;
498
+
499
+ t_style.top = componentTop - cH / 2 + 'px';
500
+ t_style[dir[0]] = (isNonSelected ? sideOffset - cW / 2 : sideOffset + w) + 'px';
501
+ t_style[dir[1]] = '';
502
+
503
+ lb_t.setAttribute('data-offset', scrollY + ',' + scrollX);
504
+ if (_overInfo) dom.utils.removeClass(lb_t, 'se-on-selected');
505
+ else dom.utils.addClass(lb_t, 'se-on-selected');
506
+
507
+ t_style.display = 'block';
508
+ t_style.visibility = '';
509
+ } else {
510
+ t_style.display = 'none';
511
+ }
512
+
513
+ // bottom
514
+ if (isList ? !container.nextSibling : !this.format.isLine(container.nextElementSibling)) {
515
+ const cStyle = _w.getComputedStyle(lb_b);
516
+ const cH = numbers.get(cStyle.height, 1);
517
+ const cW = numbers.get(cStyle.width, 1);
518
+
519
+ if (!componentTop) {
520
+ this.eventManager._lineBreakComp = container;
521
+ componentTop = top;
522
+ w = target.offsetWidth / 2 / 2;
523
+ }
524
+
525
+ b_style.top = componentTop + target.offsetHeight - cH / 2 + 'px';
526
+ b_style[dir[0]] = sideOffset + target.offsetWidth - (isNonSelected ? 0 : w) - (isNonSelected ? cW / 2 : cW) + 'px';
527
+ b_style[dir[1]] = '';
528
+
529
+ lb_b.setAttribute('data-offset', scrollY + ',' + dir[0] + ',' + scrollX);
530
+ if (_overInfo) dom.utils.removeClass(lb_b, 'se-on-selected');
531
+ else dom.utils.addClass(lb_b, 'se-on-selected');
532
+
533
+ b_style.display = 'block';
534
+ b_style.visibility = '';
535
+ } else {
536
+ b_style.display = 'none';
537
+ }
538
+ },
539
+
540
+ /**
541
+ * @private
542
+ * @this {ComponentThis}
543
+ * @description Adds global event listeners for component interactions such as copy, cut, and keydown events.
544
+ */
545
+ __addGlobalEvent() {
546
+ this.__removeGlobalEvent();
547
+ this._bindClose_copy = this.eventManager.addGlobalEvent('copy', this.__globalEvents.copy);
548
+ this._bindClose_cut = this.eventManager.addGlobalEvent('cut', this.__globalEvents.cut);
549
+ this._bindClose_keydown = this.eventManager.addGlobalEvent('keydown', this.__globalEvents.keydown);
550
+ },
551
+
552
+ /**
553
+ * @private
554
+ * @this {ComponentThis}
555
+ * @description Removes global event listeners that were previously added for component interactions.
556
+ */
557
+ __removeGlobalEvent() {
558
+ this.__removeNotFileGlobalEvent();
559
+ if (this._bindClose_copy) this._bindClose_copy = this.eventManager.removeGlobalEvent(this._bindClose_copy);
560
+ if (this._bindClose_cut) this._bindClose_cut = this.eventManager.removeGlobalEvent(this._bindClose_cut);
561
+ if (this._bindClose_keydown) this._bindClose_keydown = this.eventManager.removeGlobalEvent(this._bindClose_keydown);
562
+ },
563
+
564
+ /**
565
+ * @private
566
+ * @this {ComponentThis}
567
+ * @description Adds global event listeners for non-file-related interactions such as mouse and touch events.
568
+ */
569
+ __addNotFileGlobalEvent() {
570
+ this.__removeNotFileGlobalEvent();
571
+ this._bindClose_mousedown = this.eventManager.addGlobalEvent(isMobile ? 'click' : 'mousedown', this.__globalEvents.mousedown, true);
572
+ },
573
+
574
+ /**
575
+ * @private
576
+ * @this {ComponentThis}
577
+ * @description Removes global event listeners related to non-file interactions.
578
+ */
579
+ __removeNotFileGlobalEvent() {
580
+ if (this._bindClose_mousedown) this._bindClose_mousedown = this.eventManager.removeGlobalEvent(this._bindClose_mousedown);
581
+ },
582
+
583
+ /**
584
+ * @private
585
+ * @this {ComponentThis}
586
+ * @description Removes drag-related events and resets drag-related states.
587
+ */
588
+ _removeDragEvent() {
589
+ /** @type {HTMLElement} */ (this.carrierWrapper.querySelector('.se-drag-cursor')).style.left = '-10000px';
590
+ if (_DragHandle.get('__dragHandler')) _DragHandle.get('__dragHandler').style.display = 'none';
591
+
592
+ dom.utils.removeClass([_DragHandle.get('__dragHandler'), _DragHandle.get('__dragContainer')], 'se-dragging');
593
+ dom.utils.removeClass([_DragHandle.get('__dragCover'), _DragHandle.get('__dragContainer')], 'se-drag-over');
594
+
595
+ _DragHandle.set('__figureInst', null);
596
+ _DragHandle.set('__dragInst', null);
597
+ _DragHandle.set('__dragHandler', null);
598
+ _DragHandle.set('__dragContainer', null);
599
+ _DragHandle.set('__dragCover', null);
600
+ _DragHandle.set('__dragMove', null);
601
+ _DragHandle.set('__overInfo', null);
602
+ },
603
+
604
+ constructor: Component
605
+ };
606
+
607
+ /**
608
+ * @this {ComponentThis}
609
+ */
610
+ function OnDragEnter() {
611
+ this.editor._preventBlur = true;
612
+ this.ui._visibleControllers(false, dom.utils.hasClass(_DragHandle.get('__dragHandler'), 'se-drag-handle-full'));
613
+ dom.utils.addClass(_DragHandle.get('__dragCover') || _DragHandle.get('__dragContainer'), 'se-drag-over');
614
+ }
615
+
616
+ /**
617
+ * @this {ComponentThis}
618
+ */
619
+ function OnDragLeave() {
620
+ this.editor._preventBlur = false;
621
+ if (!dom.utils.hasClass(_DragHandle.get('__dragHandler'), 'se-drag-handle-full')) this.ui._visibleControllers(true, true);
622
+ dom.utils.removeClass([_DragHandle.get('__dragCover'), _DragHandle.get('__dragContainer')], 'se-drag-over');
623
+ }
624
+
625
+ /**
626
+ * @this {ComponentThis}
627
+ * @param {DragEvent} e - Drag event
628
+ */
629
+ function OnDragStart(e) {
630
+ const cover = _DragHandle.get('__dragCover') || _DragHandle.get('__dragContainer');
631
+
632
+ if (!cover) {
633
+ e.preventDefault();
634
+ return;
635
+ }
636
+
637
+ this.editor._preventBlur = false;
638
+ dom.utils.addClass(_DragHandle.get('__dragHandler'), 'se-dragging');
639
+ dom.utils.addClass(_DragHandle.get('__dragContainer'), 'se-dragging');
640
+ e.dataTransfer.setDragImage(cover, this.options.get('_rtl') ? cover.offsetWidth : -5, -5);
641
+ }
642
+
643
+ /**
644
+ * @this {ComponentThis}
645
+ */
646
+ function OnDragEnd() {
647
+ this.editor._preventBlur = false;
648
+ dom.utils.removeClass([_DragHandle.get('__dragHandler'), _DragHandle.get('__dragContainer')], 'se-dragging');
649
+ this._removeDragEvent();
650
+ }
651
+
652
+ /**
653
+ * @this {ComponentThis}
654
+ * @param {MouseEvent} e - Mouse event
655
+ */
656
+ function OnDragClick(e) {
657
+ const target = dom.query.getEventTarget(e);
658
+ if (!dom.utils.hasClass(target, 'se-drag-handle-full')) return;
659
+
660
+ const dragInst = _DragHandle.get('__dragInst');
661
+ if (!dragInst) return;
662
+
663
+ this._removeDragEvent();
664
+ this.select(dragInst.currentTarget, dragInst.currentPluginName);
665
+ }
666
+
667
+ /**
668
+ * @this {ComponentThis}
669
+ * @param {MouseEvent} e - Mouse event
670
+ */
671
+ function CloseListener_mousedown(e) {
672
+ const target = dom.query.getEventTarget(e);
673
+ if (
674
+ this.currentTarget?.contains(target) ||
675
+ dom.query.getParentElement(target, '.se-controller') ||
676
+ dom.utils.hasClass(target, 'se-drag-handle') ||
677
+ (this.currentPluginName === this.editor.currentControllerName && this.editor.opendControllers.some(({ form }) => form.contains(target)))
678
+ ) {
679
+ return;
680
+ }
681
+ this.deselect();
682
+ }
683
+
684
+ /**
685
+ * @this {ComponentThis}
686
+ * @param {ClipboardEvent} e - Event object
687
+ */
688
+ function OnCopy_component(e) {
689
+ const target = dom.query.getEventTarget(e);
690
+ if (dom.check.isInputElement(target) && dom.query.getParentElement(target, '.se-modal')) return;
691
+
692
+ const info = this.info;
693
+ if (!info) return;
694
+
695
+ const cloneContainer = info.container.cloneNode(true);
696
+ dom.utils.removeClass(cloneContainer, 'se-component-selected');
697
+
698
+ if (typeof this.plugins[info.pluginName]?.onCopyComponent !== 'function' || this.plugins[info.pluginName].onCopyComponent({ event: e, cloneContainer, info }) === false) {
699
+ SetClipboardComponent(e, cloneContainer, e.clipboardData);
700
+ }
701
+
702
+ // copy effect
703
+ dom.utils.flashClass(info.container, 'se-copy');
704
+ }
705
+
706
+ /**
707
+ * @this {ComponentThis}
708
+ * @param {ClipboardEvent} e - Event object
709
+ */
710
+ function OnCut_component(e) {
711
+ const info = this.info;
712
+ if (!info) return;
713
+
714
+ const cloneContainer = info.container.cloneNode(true);
715
+ dom.utils.removeClass(cloneContainer, 'se-component-selected');
716
+
717
+ SetClipboardComponent(e, cloneContainer, e.clipboardData);
718
+ this.deselect();
719
+ dom.utils.removeItem(info.container);
720
+ }
721
+
722
+ /**
723
+ * @this {ComponentThis}
724
+ * @param {KeyboardEvent} e - Event object
725
+ */
726
+ async function OnKeyDown_component(e) {
727
+ if (this.editor.selectMenuOn) return;
728
+
729
+ const keyCode = e.code;
730
+ const ctrl = keyCodeMap.isCtrl(e);
731
+
732
+ // redo, undo
733
+ if (ctrl) {
734
+ if (keyCode !== 'ControlRight' && keyCode !== 'ControlLeft') {
735
+ const info = this.editor.shortcutsKeyMap.get(keyCode + (e.shiftKey ? '1000' : ''));
736
+ if (/^(redo|undo)$/.test(info?.c)) {
737
+ e.preventDefault();
738
+ e.stopPropagation();
739
+ this.editor.run(info.c, info.t, info.e);
740
+ }
741
+ }
742
+ return;
743
+ }
744
+
745
+ // backspace key, delete key
746
+ if (keyCodeMap.isRemoveKey(keyCode)) {
747
+ e.preventDefault();
748
+ e.stopPropagation();
749
+ if (typeof this.currentPlugin?.destroy === 'function') {
750
+ await this.currentPlugin.destroy(this.currentTarget);
751
+ this.deselect();
752
+ this.editor.focus();
753
+ return;
754
+ }
755
+ }
756
+
757
+ // enter key
758
+ if (keyCodeMap.isEnter(keyCode)) {
759
+ e.preventDefault();
760
+ const compContext = this.currentInfo || this.get(this.currentTarget);
761
+ const container = compContext.container || compContext.target;
762
+ const sibling = container.previousElementSibling || container.nextElementSibling;
763
+ let newEl = null;
764
+ if (dom.check.isListCell(container.parentNode)) {
765
+ newEl = dom.utils.createElement('BR');
766
+ } else {
767
+ newEl = dom.utils.createElement(this.format.isLine(sibling) ? sibling.nodeName : this.options.get('defaultLine'), null, '<br>');
768
+ }
769
+
770
+ const pluginName = this.currentPluginName;
771
+ this.deselect();
772
+ container.parentNode.insertBefore(newEl, container);
773
+ if (this.select(compContext.target, pluginName) === false) this.editor.blur();
774
+ this.history.push(false);
775
+
776
+ return;
777
+ }
778
+
779
+ // up down, left right
780
+ if (DIR_KEYCODE.test(keyCode)) {
781
+ const { container } = this.get(this.currentTarget);
782
+ const isInline = this.isInline(container || this.currentTarget);
783
+
784
+ let el = null;
785
+ let offset = 1;
786
+ if (isInline) {
787
+ switch (keyCode) {
788
+ case 'ArrowLeft': // left
789
+ el = container.previousSibling;
790
+ offset = el?.nodeType === 3 ? el.textContent.length : 1;
791
+ break;
792
+ case 'ArrowRight': // right
793
+ el = container.nextSibling;
794
+ offset = 0;
795
+ break;
796
+ case 'ArrowUp': {
797
+ // up
798
+ const line = this.format.getLine(container, null);
799
+ el = line?.previousElementSibling;
800
+ offset = 0;
801
+ break;
802
+ }
803
+ case 'ArrowDown': {
804
+ // down
805
+ const line = this.format.getLine(container, null);
806
+ el = line?.nextElementSibling;
807
+ break;
808
+ }
809
+ }
810
+ } else {
811
+ if (DIR_UP_KEYCODE.test(keyCode)) {
812
+ el = container.previousElementSibling;
813
+ } else {
814
+ el = container.nextElementSibling;
815
+ offset = 0;
816
+ }
817
+ }
818
+
819
+ if (!el) return;
820
+
821
+ this.deselect();
822
+
823
+ const elComp = this.get(el);
824
+ if (elComp?.container) {
825
+ e.stopPropagation();
826
+ e.preventDefault();
827
+ this.select(elComp.target, elComp.pluginName);
828
+ } else {
829
+ try {
830
+ this.editor._preventBlur = true;
831
+ e.stopPropagation();
832
+ e.preventDefault();
833
+ this.selection.setRange(el, offset, el, offset);
834
+ } finally {
835
+ this.editor._preventBlur = false;
836
+ }
837
+ }
838
+
839
+ return;
840
+ }
841
+
842
+ // ESC
843
+ if (keyCodeMap.isEsc(keyCode)) {
844
+ this.deselect();
845
+ return;
846
+ }
847
+ }
848
+
849
+ function SetClipboardComponent(e, container, clipboardData) {
850
+ e.preventDefault();
851
+ e.stopPropagation();
852
+ container.querySelectorAll('.se-figure-selected').forEach((el) => dom.utils.removeClass(el, 'se-figure-selected'));
853
+ clipboardData.setData('text/html', container.outerHTML);
854
+ }
855
+
856
+ export default Component;