suneditor 3.1.4 → 3.2.1

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 (217) hide show
  1. package/dist/suneditor-contents.min.css +1 -1
  2. package/dist/suneditor.min.css +28 -1
  3. package/dist/suneditor.min.js +1 -1
  4. package/package.json +2 -1
  5. package/src/assets/design/color.css +12 -0
  6. package/src/assets/design/size.css +9 -2
  7. package/src/assets/icons/defaultIcons.js +96 -47
  8. package/src/assets/suneditor-contents.css +3 -2
  9. package/src/assets/suneditor.css +481 -31
  10. package/src/core/config/contextProvider.js +2 -1
  11. package/src/core/config/eventManager.js +41 -2
  12. package/src/core/config/optionProvider.js +23 -2
  13. package/src/core/editor.js +32 -14
  14. package/src/core/event/actions/index.js +76 -9
  15. package/src/core/event/effects/common.registry.js +2 -1
  16. package/src/core/event/effects/keydown.registry.js +169 -33
  17. package/src/core/event/effects/ruleHelpers.js +20 -6
  18. package/src/core/event/eventOrchestrator.js +162 -30
  19. package/src/core/event/handlers/handler_toolbar.js +4 -2
  20. package/src/core/event/handlers/handler_ww_dragDrop.js +10 -0
  21. package/src/core/event/handlers/handler_ww_input.js +84 -2
  22. package/src/core/event/handlers/handler_ww_key.js +82 -21
  23. package/src/core/event/handlers/handler_ww_mouse.js +45 -8
  24. package/src/core/event/ports.js +27 -13
  25. package/src/core/event/reducers/keydown.reducer.js +33 -5
  26. package/src/core/event/rules/keydown.rule.arrow.js +12 -2
  27. package/src/core/event/rules/keydown.rule.backspace.js +110 -15
  28. package/src/core/event/rules/keydown.rule.delete.js +62 -7
  29. package/src/core/event/rules/keydown.rule.enter.js +62 -31
  30. package/src/core/event/support/defaultLineManager.js +9 -2
  31. package/src/core/event/support/selectionState.js +13 -3
  32. package/src/core/kernel/coreKernel.js +1 -0
  33. package/src/core/logic/dom/char.js +13 -3
  34. package/src/core/logic/dom/format.js +171 -39
  35. package/src/core/logic/dom/html.js +350 -72
  36. package/src/core/logic/dom/inline.js +229 -34
  37. package/src/core/logic/dom/listFormat.js +111 -20
  38. package/src/core/logic/dom/nodeTransform.js +36 -11
  39. package/src/core/logic/dom/offset.js +116 -31
  40. package/src/core/logic/dom/selection.js +147 -29
  41. package/src/core/logic/panel/blockHandle.js +785 -0
  42. package/src/core/logic/panel/blockResolver.js +278 -0
  43. package/src/core/logic/panel/finder.js +16 -6
  44. package/src/core/logic/panel/menu.js +119 -18
  45. package/src/core/logic/panel/toolbar.js +75 -19
  46. package/src/core/logic/panel/viewer.js +76 -20
  47. package/src/core/logic/shell/_commandExecutor.js +44 -9
  48. package/src/core/logic/shell/commandDispatcher.js +46 -4
  49. package/src/core/logic/shell/component.js +102 -25
  50. package/src/core/logic/shell/focusManager.js +13 -3
  51. package/src/core/logic/shell/history.js +21 -6
  52. package/src/core/logic/shell/pluginManager.js +21 -13
  53. package/src/core/logic/shell/shortcuts.js +9 -1
  54. package/src/core/logic/shell/ui.js +139 -37
  55. package/src/core/schema/frameContext.js +34 -3
  56. package/src/core/schema/options.js +46 -4
  57. package/src/core/section/constructor.js +537 -106
  58. package/src/core/section/documentType.js +35 -6
  59. package/src/helper/converter.js +24 -7
  60. package/src/helper/dom/domCheck.js +25 -5
  61. package/src/helper/dom/domQuery.js +2 -1
  62. package/src/helper/dom/domUtils.js +17 -4
  63. package/src/helper/env.js +11 -2
  64. package/src/helper/keyCodeMap.js +6 -2
  65. package/src/helper/markdown.js +24 -5
  66. package/src/helper/msOffice.js +9 -2
  67. package/src/interfaces/plugins.js +1 -1
  68. package/src/langs/ckb.js +0 -1
  69. package/src/langs/cs.js +0 -1
  70. package/src/langs/da.js +0 -1
  71. package/src/langs/de.js +0 -1
  72. package/src/langs/en.js +0 -1
  73. package/src/langs/es.js +0 -1
  74. package/src/langs/fa.js +0 -1
  75. package/src/langs/fr.js +0 -1
  76. package/src/langs/he.js +0 -1
  77. package/src/langs/hu.js +0 -1
  78. package/src/langs/index.js +27 -1
  79. package/src/langs/it.js +0 -1
  80. package/src/langs/ja.js +0 -1
  81. package/src/langs/km.js +0 -1
  82. package/src/langs/ko.js +0 -1
  83. package/src/langs/lv.js +0 -1
  84. package/src/langs/nl.js +0 -1
  85. package/src/langs/pl.js +0 -1
  86. package/src/langs/pt_br.js +0 -1
  87. package/src/langs/ro.js +0 -1
  88. package/src/langs/ru.js +0 -1
  89. package/src/langs/se.js +0 -1
  90. package/src/langs/tr.js +0 -1
  91. package/src/langs/uk.js +0 -1
  92. package/src/langs/ur.js +0 -1
  93. package/src/langs/zh_cn.js +0 -1
  94. package/src/modules/contract/Browser.js +58 -14
  95. package/src/modules/contract/ColorPicker.js +12 -4
  96. package/src/modules/contract/Controller.js +37 -7
  97. package/src/modules/contract/Figure.js +156 -36
  98. package/src/modules/contract/HueSlider.js +27 -4
  99. package/src/modules/contract/Modal.js +50 -10
  100. package/src/modules/manager/FileManager.js +50 -8
  101. package/src/modules/ui/CommandMenu.js +597 -0
  102. package/src/modules/ui/ModalAnchorEditor.js +33 -6
  103. package/src/modules/ui/SelectMenu.js +481 -36
  104. package/src/modules/ui/index.js +1 -0
  105. package/src/plugins/browser/audioGallery.js +2 -1
  106. package/src/plugins/browser/fileBrowser.js +9 -2
  107. package/src/plugins/browser/fileGallery.js +2 -1
  108. package/src/plugins/browser/imageGallery.js +18 -3
  109. package/src/plugins/browser/videoGallery.js +13 -4
  110. package/src/plugins/command/blockquote.js +6 -1
  111. package/src/plugins/command/codeBlock.js +43 -5
  112. package/src/plugins/command/exportPDF.js +20 -4
  113. package/src/plugins/command/fileUpload.js +46 -11
  114. package/src/plugins/command/list_bulleted.js +6 -1
  115. package/src/plugins/command/list_numbered.js +6 -1
  116. package/src/plugins/dropdown/align.js +21 -33
  117. package/src/plugins/dropdown/backgroundColor.js +10 -2
  118. package/src/plugins/dropdown/blockStyle.js +17 -22
  119. package/src/plugins/dropdown/font.js +34 -34
  120. package/src/plugins/dropdown/hr.js +15 -43
  121. package/src/plugins/dropdown/layout.js +10 -27
  122. package/src/plugins/dropdown/lineHeight.js +10 -33
  123. package/src/plugins/dropdown/list.js +19 -25
  124. package/src/plugins/dropdown/paragraphStyle.js +16 -24
  125. package/src/plugins/dropdown/table/index.js +72 -17
  126. package/src/plugins/dropdown/table/render/table.html.js +3 -1
  127. package/src/plugins/dropdown/table/services/table.cell.js +33 -7
  128. package/src/plugins/dropdown/table/services/table.clipboard.js +5 -1
  129. package/src/plugins/dropdown/table/services/table.grid.js +40 -9
  130. package/src/plugins/dropdown/table/services/table.resize.js +55 -11
  131. package/src/plugins/dropdown/table/services/table.selection.js +27 -7
  132. package/src/plugins/dropdown/table/services/table.style.js +120 -20
  133. package/src/plugins/dropdown/template.js +13 -29
  134. package/src/plugins/dropdown/textStyle.js +22 -35
  135. package/src/plugins/field/autocomplete.js +8 -4
  136. package/src/plugins/field/slashCommand.js +297 -0
  137. package/src/plugins/index.js +3 -0
  138. package/src/plugins/input/fontSize.js +37 -8
  139. package/src/plugins/input/pageNavigator.js +7 -1
  140. package/src/plugins/modal/audio.js +71 -17
  141. package/src/plugins/modal/drawing.js +41 -11
  142. package/src/plugins/modal/embed.js +92 -22
  143. package/src/plugins/modal/image/index.js +114 -28
  144. package/src/plugins/modal/image/services/image.size.js +9 -2
  145. package/src/plugins/modal/image/services/image.upload.js +38 -4
  146. package/src/plugins/modal/link.js +9 -2
  147. package/src/plugins/modal/math.js +10 -2
  148. package/src/plugins/modal/video/index.js +112 -26
  149. package/src/plugins/modal/video/render/video.html.js +4 -1
  150. package/src/plugins/modal/video/services/video.size.js +15 -4
  151. package/src/plugins/modal/video/services/video.upload.js +10 -2
  152. package/src/plugins/popup/anchor.js +14 -2
  153. package/src/suneditor.js +8 -2
  154. package/src/themes/cobalt.css +12 -0
  155. package/src/themes/cream.css +12 -0
  156. package/src/themes/dark.css +12 -0
  157. package/src/themes/midnight.css +12 -0
  158. package/src/typedef.js +32 -21
  159. package/types/assets/icons/defaultIcons.d.ts +1 -0
  160. package/types/core/config/contextProvider.d.ts +7 -1
  161. package/types/core/config/eventManager.d.ts +37 -4
  162. package/types/core/config/optionProvider.d.ts +5 -1
  163. package/types/core/event/actions/index.d.ts +44 -6
  164. package/types/core/event/effects/keydown.registry.d.ts +37 -7
  165. package/types/core/event/effects/ruleHelpers.d.ts +6 -1
  166. package/types/core/event/eventOrchestrator.d.ts +13 -3
  167. package/types/core/event/handlers/handler_ww_clipboard.d.ts +15 -3
  168. package/types/core/event/handlers/handler_ww_dragDrop.d.ts +14 -2
  169. package/types/core/event/handlers/handler_ww_input.d.ts +10 -2
  170. package/types/core/event/handlers/handler_ww_key.d.ts +10 -2
  171. package/types/core/event/handlers/handler_ww_mouse.d.ts +25 -5
  172. package/types/core/event/ports.d.ts +11 -4
  173. package/types/core/event/reducers/keydown.reducer.d.ts +12 -3
  174. package/types/core/event/rules/keydown.rule.backspace.d.ts +6 -1
  175. package/types/core/kernel/store.d.ts +4 -1
  176. package/types/core/logic/dom/format.d.ts +32 -14
  177. package/types/core/logic/dom/html.d.ts +11 -3
  178. package/types/core/logic/dom/nodeTransform.d.ts +6 -6
  179. package/types/core/logic/panel/blockHandle.d.ts +64 -0
  180. package/types/core/logic/panel/blockResolver.d.ts +50 -0
  181. package/types/core/logic/panel/menu.d.ts +72 -3
  182. package/types/core/logic/shell/commandDispatcher.d.ts +8 -0
  183. package/types/core/logic/shell/component.d.ts +5 -3
  184. package/types/core/logic/shell/shortcuts.d.ts +10 -1
  185. package/types/core/logic/shell/ui.d.ts +7 -1
  186. package/types/core/schema/context.d.ts +7 -1
  187. package/types/core/schema/frameContext.d.ts +36 -4
  188. package/types/core/schema/options.d.ts +83 -2
  189. package/types/core/section/constructor.d.ts +38 -1
  190. package/types/events.d.ts +208 -32
  191. package/types/helper/converter.d.ts +4 -2
  192. package/types/helper/dom/domCheck.d.ts +12 -2
  193. package/types/helper/dom/domQuery.d.ts +35 -7
  194. package/types/helper/dom/domUtils.d.ts +4 -1
  195. package/types/interfaces/plugins.d.ts +2 -2
  196. package/types/langs/_Lang.d.ts +0 -1
  197. package/types/modules/contract/ColorPicker.d.ts +2 -2
  198. package/types/modules/contract/Controller.d.ts +7 -0
  199. package/types/modules/contract/Figure.d.ts +31 -4
  200. package/types/modules/ui/CommandMenu.d.ts +262 -0
  201. package/types/modules/ui/SelectMenu.d.ts +56 -3
  202. package/types/modules/ui/index.d.ts +1 -0
  203. package/types/plugins/browser/audioGallery.d.ts +1 -1
  204. package/types/plugins/browser/fileBrowser.d.ts +1 -1
  205. package/types/plugins/browser/fileGallery.d.ts +1 -1
  206. package/types/plugins/browser/imageGallery.d.ts +1 -1
  207. package/types/plugins/browser/videoGallery.d.ts +1 -1
  208. package/types/plugins/dropdown/align.d.ts +1 -1
  209. package/types/plugins/dropdown/blockStyle.d.ts +3 -1
  210. package/types/plugins/dropdown/table/index.d.ts +4 -1
  211. package/types/plugins/dropdown/table/services/table.clipboard.d.ts +5 -1
  212. package/types/plugins/dropdown/table/services/table.grid.d.ts +10 -2
  213. package/types/plugins/field/autocomplete.d.ts +0 -1
  214. package/types/plugins/field/slashCommand.d.ts +165 -0
  215. package/types/plugins/index.d.ts +3 -0
  216. package/types/plugins/modal/link.d.ts +4 -1
  217. package/types/typedef.d.ts +48 -23
@@ -0,0 +1,785 @@
1
+ import { dom, env } from '../../../helper';
2
+ import { resolveBlock } from './blockResolver';
3
+ import { ResolveButton } from '../../section/constructor';
4
+ import CommandMenu from '../../../modules/ui/CommandMenu.js';
5
+
6
+ const { _w, _d } = env;
7
+
8
+ /**
9
+ * @description Resolve a CSS length value (`px`, `em`, `rem`, or unitless) to pixels.
10
+ * Falls back to `0` when the value is empty or unparseable.
11
+ * @param {string} value Raw CSS length string (e.g. `"1.5em"`)
12
+ * @param {CSSStyleDeclaration} contextStyle Computed style of the element using the value (for `em`)
13
+ * @returns {number} Resolved pixel length
14
+ */
15
+ function _resolveLengthPx(value, contextStyle) {
16
+ const trimmed = (value || '').trim();
17
+ const num = parseFloat(trimmed);
18
+ if (!num) return 0;
19
+ if (/rem$/.test(trimmed)) {
20
+ return num * (parseFloat(_w.getComputedStyle(_d.documentElement).fontSize) || 16);
21
+ }
22
+ if (/em$/.test(trimmed)) {
23
+ return num * (parseFloat(contextStyle.fontSize) || 16);
24
+ }
25
+ return num;
26
+ }
27
+
28
+ /**
29
+ * @class
30
+ * @description Block handle UI — appears in the left gutter on block hover.
31
+ */
32
+ class BlockHandle {
33
+ #$;
34
+
35
+ #area;
36
+ #handle;
37
+ #plusBtn;
38
+ #dragBtn;
39
+ #menuConfig;
40
+
41
+ /** @type {CommandMenu|null} */
42
+ #actionMenu = null;
43
+ /** @type {HTMLElement|null} */
44
+ #currentBlock = null;
45
+ /** @type {number|null} */
46
+ #rafId = null;
47
+ /** @type {Node|null} */
48
+ #pendingTarget = null;
49
+ /** @type {number|undefined} */
50
+ #pendingMouseY = undefined;
51
+ /** @type {number|null} */
52
+ #hideTimer = null;
53
+ /** @type {HTMLElement[]} */
54
+ #hoverLines = [];
55
+
56
+ // Drag state
57
+ /** @type {boolean} */
58
+ #isDragging = false;
59
+ /** @type {HTMLElement|null} */
60
+ #dragIndicator = null;
61
+ /** @type {HTMLElement|null} */
62
+ #dragTarget = null;
63
+ /** @type {{element: HTMLElement, position: 'before'|'after'}|null} */
64
+ #dropTarget = null;
65
+ /** @type {HTMLElement|null} */
66
+ #dragWysiwyg = null;
67
+ /** @type {HTMLElement[]|null} */
68
+ #dragChildren = null;
69
+
70
+ /**
71
+ * @constructor
72
+ * @param {SunEditor.Deps} $ - Kernel dependencies
73
+ * @param {HTMLElement} blockHandleArea - Container (.se-block-handle-area)
74
+ * @param {HTMLElement} blockHandle - Handle group (.se-block-handle)
75
+ * @param {HTMLElement} blockHandlePlus - Plus button
76
+ * @param {HTMLElement} blockHandleDrag - Drag button
77
+ * @param {Array<string | { title: string, icon?: string, action: function(SunEditor.Deps, { block: HTMLElement }): void }>|null} menuConfig
78
+ * Menu entries. Strings resolve via `ResolveButton` (plugin names, built-in commands). Objects
79
+ * define a custom row whose `action` is invoked with the Deps bag and the current block element.
80
+ */
81
+ constructor($, blockHandleArea, blockHandle, blockHandlePlus, blockHandleDrag, menuConfig) {
82
+ this.#$ = $;
83
+ this.#area = blockHandleArea;
84
+ this.#handle = blockHandle;
85
+ this.#plusBtn = blockHandlePlus;
86
+ this.#dragBtn = blockHandleDrag;
87
+ this.#menuConfig = menuConfig || null;
88
+
89
+ this.#$.contextProvider.carrierWrapper.appendChild(this.#handle);
90
+
91
+ const em = this.#$.eventManager;
92
+
93
+ // Prevent editor blur on handle interaction — preserves selection range (same as toolbar)
94
+ em.addEvent(this.#handle, 'mousedown', this.#onHandleMouseDown.bind(this));
95
+
96
+ // Create drag indicator
97
+ this.#dragIndicator = dom.utils.createElement('DIV', { class: 'se-block-drag-indicator' });
98
+ this.#dragIndicator.style.display = 'none';
99
+ this.#area.parentElement?.appendChild(this.#dragIndicator);
100
+
101
+ em.addEvent(this.#plusBtn, 'click', this.#onPlusClick.bind(this));
102
+ // HTML5 drag-and-drop: native drag image, native event flow.
103
+ this.#dragBtn.draggable = true;
104
+ em.addEvent(this.#dragBtn, 'dragstart', this.#onDragStart.bind(this));
105
+ em.addEvent(this.#dragBtn, 'dragend', this.#onDragEnd.bind(this));
106
+ em.addEvent(this.#dragBtn, 'click', this.#onDragClick.bind(this));
107
+ em.addEvent(this.#area, 'mousemove', this.#onAreaMouseMove.bind(this));
108
+ em.addEvent(this.#area, 'mouseleave', this.#onAreaMouseLeave.bind(this));
109
+
110
+ // Wrapper (parent of area + wysiwyg) — catches mouse leaving the entire editor
111
+ // zone, plus the dragover/drop targets for the in-flight HTML5 drag.
112
+ const wrapper = this.#area.parentElement;
113
+ if (wrapper) {
114
+ em.addEvent(wrapper, 'mouseleave', this.#onWrapperMouseLeave.bind(this));
115
+ em.addEvent(wrapper, 'dragover', this.#onDragOver.bind(this));
116
+ em.addEvent(wrapper, 'drop', this.#onDrop.bind(this));
117
+ }
118
+
119
+ em.addEvent(this.#$.frameContext.get('eventWysiwyg'), 'keydown', this.#onWrapperKeyDown.bind(this), true);
120
+ }
121
+
122
+ /**
123
+ * @description Position the block handle for the given mouse target. Uses rAF throttle.
124
+ * Called from wysiwyg mousemove.
125
+ * @param {Node} eventTarget - The element under the mouse cursor
126
+ * @param {number} [mouseY] - Mouse clientY for nested list resolution
127
+ */
128
+ positionForTarget(eventTarget, mouseY) {
129
+ this.#cancelHide();
130
+ // Lock handle while action menu is open or dragging
131
+ if (this.#actionMenu?.isOpen || this.#isDragging) return;
132
+ this.#pendingTarget = eventTarget;
133
+ this.#pendingMouseY = mouseY;
134
+ if (this.#rafId) return;
135
+ this.#rafId = _w.requestAnimationFrame(() => {
136
+ this.#rafId = null;
137
+ if (this.#pendingTarget) {
138
+ this.#doPosition(this.#pendingTarget, this.#pendingMouseY);
139
+ }
140
+ });
141
+ }
142
+
143
+ /**
144
+ * @description Schedule hiding the block handle with a short delay.
145
+ * @param {MouseEvent} e - Mouse event
146
+ */
147
+ hide(e) {
148
+ if (this.#actionMenu?.isOpen) return;
149
+ const related = /** @type {Node} */ (e?.relatedTarget);
150
+ if (related && (this.#area.contains(related) || this.#handle?.contains(related))) return;
151
+ this.#scheduleHide();
152
+ }
153
+
154
+ /**
155
+ * @description Immediately hide the block handle (no delay).
156
+ */
157
+ hideNow() {
158
+ this.#cancelHide();
159
+ this.#handle.style.display = 'none';
160
+ this.#actionMenu?.close();
161
+ this.#setCurrentBlock(null);
162
+ }
163
+
164
+ /**
165
+ * @description Sync handle position on editor scroll. Closes menu if open.
166
+ */
167
+ syncScroll() {
168
+ if (!this.#currentBlock) return;
169
+ // Skip transition during scroll — handle should track scroll instantly
170
+ dom.utils.addClass(this.#handle, 'se-no-transition');
171
+ this.#updatePosition(this.#currentBlock);
172
+ void this.#handle.offsetHeight;
173
+ dom.utils.removeClass(this.#handle, 'se-no-transition');
174
+
175
+ if (this.#actionMenu?.isOpen) {
176
+ this.#actionMenu.setHidden(this.#handle.style.display === 'none');
177
+ }
178
+ }
179
+
180
+ /**
181
+ * @description Cleanup — remove listeners, destroy menus, null references.
182
+ */
183
+ destroy() {
184
+ if (this.#rafId) {
185
+ _w.cancelAnimationFrame(this.#rafId);
186
+ this.#rafId = null;
187
+ }
188
+ this.#isDragging = false;
189
+ this.#cancelHide();
190
+
191
+ if (this.#dragBtn) this.#dragBtn.draggable = false;
192
+ this.#dragIndicator?.remove();
193
+ this.#handle?.remove();
194
+ // CommandMenu's closeMethod tears down its own flyout when the menu closes.
195
+ this.#actionMenu?.close();
196
+ this.#actionMenu = null;
197
+ this.#setCurrentBlock(null);
198
+ this.#pendingTarget = null;
199
+ this.#pendingMouseY = undefined;
200
+ this.#$ = null;
201
+ this.#area = null;
202
+ this.#handle = null;
203
+ this.#plusBtn = null;
204
+ this.#dragBtn = null;
205
+ }
206
+
207
+ #setCurrentBlock(block) {
208
+ this.#clearHoverLines();
209
+ this.#currentBlock = block;
210
+ if (!block) return;
211
+
212
+ // If a non-collapsed selection covers multiple lines including this block,
213
+ // highlight all lines in the selection (action menu will affect all of them).
214
+ const rangeLines = this.#getSelectionLinesContaining(block);
215
+ if (rangeLines) {
216
+ this.#hoverLines = rangeLines;
217
+ for (let i = 0; i < rangeLines.length; i++) dom.utils.addClass(rangeLines[i], 'se-block-hover');
218
+ } else {
219
+ dom.utils.addClass(block, 'se-block-hover');
220
+ }
221
+ }
222
+
223
+ /**
224
+ * @description Return all lines covered by the current selection if it spans
225
+ * multiple lines and includes `block`. Otherwise null.
226
+ * @param {HTMLElement} block
227
+ * @returns {?Array<HTMLElement>}
228
+ */
229
+ #getSelectionLinesContaining(block) {
230
+ try {
231
+ const range = this.#$.selection.getRange();
232
+ if (!range || range.collapsed) return null;
233
+ const startLine = this.#$.format.getLine(range.startContainer, null);
234
+ const endLine = this.#$.format.getLine(range.endContainer, null);
235
+ if (!startLine || !endLine || startLine === endLine) return null;
236
+
237
+ const all = dom.query.getListChildren(range.commonAncestorContainer, (n) => this.#$.format.isLine(n), null);
238
+ const sIdx = all.indexOf(startLine);
239
+ const eIdx = all.indexOf(endLine);
240
+ if (sIdx === -1 || eIdx === -1) return null;
241
+ const lines = all.slice(Math.min(sIdx, eIdx), Math.max(sIdx, eIdx) + 1);
242
+ if (lines.indexOf(block) === -1) return null;
243
+ return lines;
244
+ } catch {
245
+ // No valid selection
246
+ }
247
+ return null;
248
+ }
249
+
250
+ #setHoverLines(lines) {
251
+ this.#clearHoverLines();
252
+ this.#hoverLines = lines || [];
253
+ for (let i = 0; i < this.#hoverLines.length; i++) {
254
+ dom.utils.addClass(this.#hoverLines[i], 'se-block-hover');
255
+ }
256
+ }
257
+
258
+ #clearHoverLines() {
259
+ if (this.#currentBlock) dom.utils.removeClass(this.#currentBlock, 'se-block-hover');
260
+ for (let i = 0; i < this.#hoverLines.length; i++) {
261
+ dom.utils.removeClass(this.#hoverLines[i], 'se-block-hover');
262
+ }
263
+ this.#hoverLines = [];
264
+ }
265
+
266
+ #scheduleHide() {
267
+ if (this.#hideTimer) return;
268
+ this.#hideTimer = _w.setTimeout(() => {
269
+ this.#hideTimer = null;
270
+ this.#handle.style.display = 'none';
271
+ this.#actionMenu?.close();
272
+ this.#setCurrentBlock(null);
273
+ }, 200);
274
+ }
275
+
276
+ #cancelHide() {
277
+ if (this.#hideTimer) {
278
+ _w.clearTimeout(this.#hideTimer);
279
+ this.#hideTimer = null;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * @description Mouse moves inside the block handle area.
285
+ * Probes the wysiwyg at the same Y level to find which block to show the handle for.
286
+ * @param {MouseEvent} e
287
+ */
288
+ #onAreaMouseMove(e) {
289
+ if (!this.#$) return;
290
+ if (this.#isDragging) return;
291
+
292
+ this.#cancelHide();
293
+
294
+ // Don't reposition while action menu is open
295
+ if (this.#actionMenu?.isOpen) return;
296
+
297
+ const wysiwygEl = this.#$.frameContext.get('wysiwyg');
298
+ if (!wysiwygEl) return;
299
+
300
+ const frameEl = this.#$.frameContext.get('wysiwygFrame');
301
+ const isIframe = frameEl && frameEl.nodeName === 'IFRAME';
302
+
303
+ let target;
304
+ if (isIframe) {
305
+ // iframe mode: convert to iframe-local coordinates
306
+ const iframeRect = frameEl.getBoundingClientRect();
307
+ const doc = /** @type {HTMLIFrameElement} */ (frameEl).contentDocument;
308
+ if (doc) target = doc.elementFromPoint(iframeRect.width / 2, e.clientY - iframeRect.top);
309
+ } else {
310
+ // Normal mode: probe the wysiwyg center at the mouse's Y
311
+ const rect = wysiwygEl.getBoundingClientRect();
312
+ target = document.elementFromPoint(rect.left + rect.width / 2, e.clientY);
313
+ }
314
+
315
+ if (target && wysiwygEl.contains(target)) {
316
+ this.#doPosition(target, e.clientY);
317
+ }
318
+ }
319
+
320
+ /**
321
+ * @description Mouse leaves the block handle area.
322
+ * @param {MouseEvent} e
323
+ */
324
+ #onAreaMouseLeave(e) {
325
+ if (this.#actionMenu?.isOpen) return;
326
+
327
+ const related = /** @type {Node} */ (e?.relatedTarget);
328
+ if (related && this.#handle?.contains(related)) return;
329
+
330
+ this.#scheduleHide();
331
+ }
332
+
333
+ /**
334
+ * @description Mouse leaves the entire wrapper (area + wysiwyg).
335
+ */
336
+ #onWrapperMouseLeave(e) {
337
+ if (this.#actionMenu?.isOpen) return;
338
+ const related = /** @type {Node} */ (e?.relatedTarget);
339
+ if (related && this.#handle?.contains(related)) return;
340
+ this.#cancelHide();
341
+ this.#handle.style.display = 'none';
342
+ this.#setCurrentBlock(null);
343
+ }
344
+
345
+ /**
346
+ * @param {Node} eventTarget
347
+ * @param {number} [mouseY]
348
+ */
349
+ #doPosition(eventTarget, mouseY) {
350
+ if (!this.#$) return; // destroyed
351
+
352
+ const format = this.#$.format;
353
+ const wysiwygFrame = this.#$.frameContext.get('wysiwyg');
354
+ if (!wysiwygFrame) return;
355
+
356
+ let target = eventTarget;
357
+ if (target === wysiwygFrame && mouseY !== undefined) {
358
+ const rect = wysiwygFrame.getBoundingClientRect();
359
+ const probed = document.elementFromPoint(rect.left + rect.width / 2, mouseY);
360
+ if (probed && wysiwygFrame.contains(probed) && probed !== wysiwygFrame) {
361
+ target = probed;
362
+ }
363
+ }
364
+
365
+ const block = resolveBlock(
366
+ target,
367
+ {
368
+ getLine: (node, validation) => format.getLine(node, validation),
369
+ getBlock: (el, validation) => format.getBlock(el, validation),
370
+ isLine: (el) => format.isLine(el),
371
+ isBlock: (el) => format.isBlock(el),
372
+ },
373
+ wysiwygFrame,
374
+ mouseY,
375
+ );
376
+
377
+ if (!block) {
378
+ this.#scheduleHide();
379
+ return;
380
+ }
381
+
382
+ // Same block and handle already visible — skip
383
+ if (block.element === this.#currentBlock && this.#handle.style.display === 'flex') return;
384
+
385
+ if (this.#currentBlock?.isConnected && this.#handle.style.display === 'flex' && mouseY !== undefined) {
386
+ if (!this.#currentBlock.contains(block.element)) {
387
+ const r = this.#currentBlock.getBoundingClientRect();
388
+ if (mouseY >= r.top && mouseY <= r.bottom) return;
389
+ }
390
+ }
391
+
392
+ this.#setCurrentBlock(block.element);
393
+ this.#updatePosition(block.element);
394
+ }
395
+
396
+ /**
397
+ * @param {HTMLElement} blockElement
398
+ */
399
+ #updatePosition(blockElement) {
400
+ if (!blockElement.isConnected) {
401
+ this.hideNow();
402
+ return;
403
+ }
404
+
405
+ const blockRect = blockElement.getBoundingClientRect();
406
+ const areaRect = this.#area.getBoundingClientRect();
407
+
408
+ const wysiwygFrameEl = this.#$.frameContext.get('wysiwygFrame');
409
+ const isIframe = /^iframe$/i.test(wysiwygFrameEl.nodeName);
410
+ const iframeRect = isIframe ? wysiwygFrameEl.getBoundingClientRect() : null;
411
+
412
+ if (isIframe) {
413
+ const iframeH = wysiwygFrameEl.clientHeight || 0;
414
+ if (blockRect.bottom <= 0 || blockRect.top >= iframeH) {
415
+ this.#handle.style.display = 'none';
416
+ return;
417
+ }
418
+ } else {
419
+ const wwFrameRect = wysiwygFrameEl.getBoundingClientRect();
420
+ if (blockRect.bottom <= wwFrameRect.top || blockRect.top >= wwFrameRect.bottom) {
421
+ this.#handle.style.display = 'none';
422
+ return;
423
+ }
424
+ }
425
+
426
+ const scrollX = _w.scrollX;
427
+ const scrollY = _w.scrollY;
428
+
429
+ // parent-viewport top to convert to parent coordinates
430
+ const blockTopVP = isIframe ? blockRect.top + iframeRect.top : blockRect.top;
431
+ const top = blockTopVP + scrollY;
432
+
433
+ // Handle inline offset
434
+ const isRtl = !!this.#$.options.get('_rtl');
435
+ const indent = this.#getBlockIndent(blockElement, isRtl);
436
+
437
+ // innerWidth
438
+ let centeringExtra = 0;
439
+ if (this.#$.frameOptions.get('innerWidth')) {
440
+ const wysiwygFrame = this.#$.frameContext.get('wysiwyg');
441
+ if (wysiwygFrame) {
442
+ const cs = _w.getComputedStyle(wysiwygFrame);
443
+ const padStart = parseFloat(isRtl ? cs.paddingRight : cs.paddingLeft) || 0;
444
+ const baseline = _resolveLengthPx(cs.getPropertyValue('--se-edit-inner-padding'), cs);
445
+ centeringExtra = Math.max(0, padStart - baseline);
446
+ }
447
+ }
448
+
449
+ const totalOffset = indent + centeringExtra;
450
+
451
+ // First appearance after being hidden — skip transition
452
+ const wasHidden = this.#handle.style.display !== 'flex';
453
+ if (wasHidden) dom.utils.addClass(this.#handle, 'se-no-transition');
454
+
455
+ this.#handle.style.top = top + 'px';
456
+ if (isRtl) {
457
+ this.#handle.style.left = '';
458
+ this.#handle.style.right = _w.innerWidth - areaRect.right - scrollX + totalOffset + 'px';
459
+ } else {
460
+ this.#handle.style.right = '';
461
+ this.#handle.style.left = areaRect.left + scrollX + totalOffset + 'px';
462
+ }
463
+ this.#handle.style.display = 'flex';
464
+
465
+ if (wasHidden) {
466
+ void this.#handle.offsetHeight;
467
+ dom.utils.removeClass(this.#handle, 'se-no-transition');
468
+ }
469
+ }
470
+
471
+ /**
472
+ * @description Calculate the handle's inline indent.
473
+ * @param {HTMLElement} blockElement
474
+ * @param {boolean} isRtl
475
+ * @returns {number} Indent in pixels (0 = default gutter position)
476
+ */
477
+ #getBlockIndent(blockElement, isRtl) {
478
+ const wysiwyg = this.#$.frameContext.get('wysiwyg');
479
+ if (!wysiwyg) return 0;
480
+
481
+ const format = this.#$.format;
482
+ const marginKey = isRtl ? 'marginRight' : 'marginLeft';
483
+ const paddingKey = isRtl ? 'paddingRight' : 'paddingLeft';
484
+ let indent = 0;
485
+
486
+ indent += parseFloat(_w.getComputedStyle(blockElement)[marginKey]) || 0;
487
+
488
+ let el = blockElement.parentElement;
489
+ while (el && el !== wysiwyg) {
490
+ if (format.isBlock(el)) {
491
+ const s = _w.getComputedStyle(el);
492
+ indent += (parseFloat(s[paddingKey]) || 0) + (parseFloat(s[marginKey]) || 0);
493
+ }
494
+ el = el.parentElement;
495
+ }
496
+
497
+ return indent;
498
+ }
499
+
500
+ /**
501
+ * @description Expand the selection range so it covers full lines.
502
+ */
503
+ #expandRangeToFullLines() {
504
+ if (!this.#currentBlock) return;
505
+ const block = this.#currentBlock;
506
+
507
+ const rangeLines = this.#getSelectionLinesContaining(block);
508
+ const firstLine = rangeLines ? rangeLines[0] : block;
509
+ const lastLine = rangeLines ? rangeLines[rangeLines.length - 1] : block;
510
+
511
+ this.#$.selection.setRange(firstLine, 0, lastLine, lastLine.childNodes.length);
512
+ }
513
+
514
+ /**
515
+ * @description Plus button click — insert new line after current block.
516
+ * Mirrors Enter-at-end-of-line behavior from keydown.rule.enter.
517
+ * @param {MouseEvent} e
518
+ */
519
+ #onPlusClick(e) {
520
+ e.preventDefault();
521
+ e.stopPropagation();
522
+
523
+ if (!this.#currentBlock) return;
524
+
525
+ const newLine = this.#$.format.addLineAfter(this.#currentBlock);
526
+ if (newLine) {
527
+ this.#$.selection.setRange(newLine, 1, newLine, 1);
528
+ this.#$.history.push(false);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * @description Handle group mousedown — prevents editor blur on click so the selection survives.
534
+ * @param {MouseEvent} e
535
+ */
536
+ #onHandleMouseDown(e) {
537
+ const onDragBtn =
538
+ this.#dragBtn && (e.target === this.#dragBtn || this.#dragBtn.contains(/** @type {Node} */ (e.target)));
539
+ if (env.isMobile) {
540
+ this.#$.store.set('_preventBlur', true);
541
+ return;
542
+ }
543
+ if (!onDragBtn) e.preventDefault();
544
+ }
545
+
546
+ /**
547
+ * @description Editor keyboard activity (typing, Enter, etc.)
548
+ */
549
+ #onWrapperKeyDown() {
550
+ this.#clearHoverLines();
551
+ }
552
+
553
+ /**
554
+ * @description Dragstart on the drag handle.
555
+ * @param {DragEvent} e
556
+ */
557
+ #onDragStart(e) {
558
+ if (!this.#currentBlock) {
559
+ e.preventDefault();
560
+ return;
561
+ }
562
+ this.#dragTarget = this.#currentBlock;
563
+ this.#isDragging = true;
564
+ this.#actionMenu?.close();
565
+ dom.utils.addClass(this.#dragTarget, 'se-block-dragging');
566
+
567
+ // Cache wysiwyg + children for the duration of the drag — DOM is stable until drop.
568
+ const wysiwygEl = /** @type {HTMLElement} */ (this.#$.frameContext.get('wysiwyg'));
569
+ this.#dragWysiwyg = wysiwygEl || null;
570
+ this.#dragChildren = wysiwygEl ? /** @type {HTMLElement[]} */ (Array.from(wysiwygEl.children)) : null;
571
+
572
+ const isRtl = !!this.#$.options.get('_rtl');
573
+ const cover = /** @type {HTMLElement} */ (this.#dragTarget);
574
+
575
+ e.dataTransfer.setDragImage(cover, isRtl ? cover.offsetWidth : -5, -5);
576
+ e.dataTransfer.effectAllowed = 'move';
577
+ e.dataTransfer.setData('application/x-suneditor-block-drag', '1');
578
+ }
579
+
580
+ /**
581
+ * @description Dragover on the wrapper.
582
+ * @param {DragEvent} e
583
+ */
584
+ #onDragOver(e) {
585
+ if (!this.#isDragging) return;
586
+ e.preventDefault();
587
+ e.stopPropagation();
588
+ e.dataTransfer.dropEffect = 'move';
589
+ this.#updateDragIndicator(e.clientY);
590
+ }
591
+
592
+ /**
593
+ * @description Native HTML5 drop on the wrapper. Commits the move.
594
+ * @param {DragEvent} e
595
+ */
596
+ #onDrop(e) {
597
+ if (!this.#isDragging) return;
598
+ e.preventDefault();
599
+ e.stopPropagation();
600
+ this.#executeDrop();
601
+ }
602
+
603
+ /**
604
+ * @description Dragend on the drag handle.
605
+ */
606
+ #onDragEnd() {
607
+ if (this.#dragTarget) dom.utils.removeClass(this.#dragTarget, 'se-block-dragging');
608
+ this.#dragIndicator.style.display = 'none';
609
+ this.#isDragging = false;
610
+ this.#dragTarget = null;
611
+ this.#dropTarget = null;
612
+ this.#dragWysiwyg = null;
613
+ this.#dragChildren = null;
614
+
615
+ if (this.#dragBtn?.parentNode) {
616
+ const parent = this.#dragBtn.parentNode;
617
+ const next = this.#dragBtn.nextSibling;
618
+ parent.removeChild(this.#dragBtn);
619
+ parent.insertBefore(this.#dragBtn, next);
620
+ }
621
+ }
622
+
623
+ /**
624
+ * @description Update drag indicator position based on mouse Y.
625
+ * @param {number} clientY
626
+ */
627
+ #updateDragIndicator(clientY) {
628
+ if (!this.#$ || !this.#dragTarget) return;
629
+
630
+ const wysiwygEl = this.#dragWysiwyg;
631
+ const children = this.#dragChildren;
632
+ if (!wysiwygEl || !children) return;
633
+
634
+ const wysiwygRect = wysiwygEl.getBoundingClientRect();
635
+ const wrapperRect = this.#area.parentElement.getBoundingClientRect();
636
+
637
+ let closest = null;
638
+
639
+ /** @type {"after"|"before"} */
640
+ let closestPos = 'after';
641
+ let closestY = 0;
642
+ let minDist = Infinity;
643
+
644
+ for (let i = 0; i < children.length; i++) {
645
+ const child = /** @type {HTMLElement} */ (children[i]);
646
+ const r = child.getBoundingClientRect();
647
+
648
+ // Check top edge
649
+ const topDist = Math.abs(clientY - r.top);
650
+ if (topDist < minDist) {
651
+ minDist = topDist;
652
+ closest = child;
653
+ closestPos = 'before';
654
+ closestY = r.top;
655
+ }
656
+ // Check bottom edge
657
+ const bottomDist = Math.abs(clientY - r.bottom);
658
+ if (bottomDist < minDist) {
659
+ minDist = bottomDist;
660
+ closest = child;
661
+ closestPos = 'after';
662
+ closestY = r.bottom;
663
+ }
664
+ }
665
+
666
+ if (closest && closest !== this.#dragTarget) {
667
+ this.#dropTarget = { element: closest, position: closestPos };
668
+ const ind = this.#dragIndicator;
669
+ ind.style.display = 'block';
670
+ ind.style.top = closestY - wrapperRect.top + 'px';
671
+ ind.style.left = wysiwygRect.left - wrapperRect.left + 'px';
672
+ ind.style.width = wysiwygRect.width + 'px';
673
+ } else {
674
+ this.#dropTarget = null;
675
+ this.#dragIndicator.style.display = 'none';
676
+ }
677
+ }
678
+
679
+ /**
680
+ * @description Execute the drop — move the dragged block to the drop position.
681
+ */
682
+ #executeDrop() {
683
+ const target = this.#dragTarget;
684
+ const drop = this.#dropTarget;
685
+
686
+ if (!target || !drop || drop.element === target) {
687
+ return;
688
+ }
689
+
690
+ const parent = target.parentNode;
691
+ if (!parent) return;
692
+
693
+ if (drop.position === 'before') {
694
+ drop.element.parentNode.insertBefore(target, drop.element);
695
+ } else {
696
+ drop.element.parentNode.insertBefore(target, drop.element.nextSibling);
697
+ }
698
+
699
+ this.#setCurrentBlock(target);
700
+ this.#updatePosition(target);
701
+ this.#$.history.push(false);
702
+ }
703
+
704
+ /**
705
+ * @description Drag button click — open action menu.
706
+ * @param {MouseEvent} e
707
+ */
708
+ #onDragClick(e) {
709
+ e.preventDefault();
710
+ e.stopPropagation();
711
+
712
+ // Skip if this click was actually a drag
713
+ if (this.#isDragging) return;
714
+
715
+ if (!this.#menuConfig) return;
716
+
717
+ // Lazy build — plugins are not yet instantiated when BlockHandle is constructed
718
+ if (!this.#actionMenu) {
719
+ this.#actionMenu = this.#buildActionMenu();
720
+ }
721
+
722
+ if (this.#actionMenu.isOpen) {
723
+ this.#actionMenu.close();
724
+ } else {
725
+ this.#expandRangeToFullLines();
726
+
727
+ // Highlight selected range lines
728
+ const lines = this.#$.format.getLines(null);
729
+ if (lines.length > 0) {
730
+ this.#setHoverLines(lines);
731
+ }
732
+
733
+ // Choose open direction based on available space.
734
+ const btnGlobal = this.#$.offset.getGlobal(this.#dragBtn);
735
+ const spaceBelow = dom.utils.getClientSize().h - (btnGlobal.top - _w.scrollY + btnGlobal.height);
736
+ const spaceAbove = btnGlobal.top - _w.scrollY;
737
+ const horiz = this.#$.options.get('_rtl') ? 'left' : 'right';
738
+ const dir = `${horiz}-${spaceBelow >= spaceAbove ? 'bottom' : 'top'}`;
739
+ this.#actionMenu.open(dir);
740
+ }
741
+ }
742
+
743
+ /**
744
+ * @description Build the action menu via {@link CommandMenu}.
745
+ * @returns {CommandMenu}
746
+ */
747
+ #buildActionMenu() {
748
+ const menu = new CommandMenu(this, this.#$, {
749
+ items: this.#menuConfig,
750
+ resolveButton: ResolveButton,
751
+ selectMenuParams: {
752
+ position: 'right-top',
753
+ dir: this.#$.options.get('_rtl') ? 'rtl' : 'ltr',
754
+ minWidth: '200px',
755
+ keydownTarget: _w,
756
+ closeMethod: () => {
757
+ dom.utils.removeClass(this.#dragBtn, 'on');
758
+ this.#clearHoverLines();
759
+ },
760
+ openMethod: () => {
761
+ dom.utils.addClass(this.#dragBtn, 'on');
762
+ },
763
+ },
764
+ });
765
+
766
+ menu.attach(this.#dragBtn, this.#onActionSelect.bind(this), { class: 'se-block-action-menu' });
767
+ menu.createRows(menu.getItems());
768
+ return menu;
769
+ }
770
+
771
+ /**
772
+ * @description Action menu click — delegates to CommandMenu.dispatch with `{block}` as the
773
+ * custom-action context. CommandMenu handles plugin dispatch (modal/browser/popup/command,
774
+ * built-ins via commandDispatcher) and dropdown-free toggling internally.
775
+ * @param {*} item
776
+ */
777
+ #onActionSelect(item) {
778
+ if (!this.#currentBlock || !this.#$ || !item) return;
779
+ this.#actionMenu?.dispatch(item, { block: this.#currentBlock });
780
+ // Dropdown-free items keep the menu open (the flyout is the actual UI); other dispatches close.
781
+ if (item.kind !== 'dropdownFree') this.#actionMenu?.close();
782
+ }
783
+ }
784
+
785
+ export default BlockHandle;