suneditor 3.1.3 → 3.2.0

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 (219) 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 +480 -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 +136 -23
  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 +9 -2
  22. package/src/core/event/handlers/handler_ww_key.js +86 -17
  23. package/src/core/event/handlers/handler_ww_mouse.js +45 -8
  24. package/src/core/event/ports.js +23 -5
  25. package/src/core/event/reducers/keydown.reducer.js +15 -2
  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 +357 -87
  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 +61 -19
  57. package/src/core/section/constructor.js +537 -103
  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 +68 -23
  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 +159 -36
  98. package/src/modules/contract/HueSlider.js +27 -4
  99. package/src/modules/contract/Modal.js +62 -11
  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 +41 -9
  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 +8 -1
  106. package/src/plugins/browser/fileBrowser.js +15 -2
  107. package/src/plugins/browser/fileGallery.js +8 -1
  108. package/src/plugins/browser/imageGallery.js +24 -3
  109. package/src/plugins/browser/videoGallery.js +19 -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 +132 -22
  143. package/src/plugins/modal/image/index.js +115 -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 +6 -1
  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 +7 -2
  173. package/types/core/event/rules/keydown.rule.backspace.d.ts +6 -1
  174. package/types/core/kernel/store.d.ts +4 -1
  175. package/types/core/logic/dom/format.d.ts +32 -14
  176. package/types/core/logic/dom/html.d.ts +11 -3
  177. package/types/core/logic/dom/nodeTransform.d.ts +6 -6
  178. package/types/core/logic/panel/blockHandle.d.ts +64 -0
  179. package/types/core/logic/panel/blockResolver.d.ts +50 -0
  180. package/types/core/logic/panel/menu.d.ts +72 -3
  181. package/types/core/logic/shell/commandDispatcher.d.ts +8 -0
  182. package/types/core/logic/shell/component.d.ts +5 -3
  183. package/types/core/logic/shell/shortcuts.d.ts +10 -1
  184. package/types/core/logic/shell/ui.d.ts +7 -1
  185. package/types/core/schema/context.d.ts +7 -1
  186. package/types/core/schema/frameContext.d.ts +36 -4
  187. package/types/core/schema/options.d.ts +110 -37
  188. package/types/core/section/constructor.d.ts +38 -1
  189. package/types/events.d.ts +208 -32
  190. package/types/helper/converter.d.ts +4 -2
  191. package/types/helper/dom/domCheck.d.ts +12 -2
  192. package/types/helper/dom/domQuery.d.ts +35 -7
  193. package/types/helper/dom/domUtils.d.ts +4 -1
  194. package/types/interfaces/plugins.d.ts +2 -2
  195. package/types/langs/_Lang.d.ts +0 -1
  196. package/types/modules/contract/Browser.d.ts +7 -7
  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/ModalAnchorEditor.d.ts +6 -1
  202. package/types/modules/ui/SelectMenu.d.ts +56 -3
  203. package/types/modules/ui/index.d.ts +1 -0
  204. package/types/plugins/browser/audioGallery.d.ts +17 -1
  205. package/types/plugins/browser/fileBrowser.d.ts +17 -1
  206. package/types/plugins/browser/fileGallery.d.ts +17 -1
  207. package/types/plugins/browser/imageGallery.d.ts +17 -1
  208. package/types/plugins/browser/videoGallery.d.ts +17 -1
  209. package/types/plugins/dropdown/align.d.ts +1 -1
  210. package/types/plugins/dropdown/blockStyle.d.ts +3 -1
  211. package/types/plugins/dropdown/table/index.d.ts +4 -1
  212. package/types/plugins/dropdown/table/services/table.clipboard.d.ts +5 -1
  213. package/types/plugins/dropdown/table/services/table.grid.d.ts +10 -2
  214. package/types/plugins/field/autocomplete.d.ts +0 -1
  215. package/types/plugins/field/slashCommand.d.ts +165 -0
  216. package/types/plugins/index.d.ts +3 -0
  217. package/types/plugins/modal/embed.d.ts +34 -0
  218. package/types/plugins/modal/link.d.ts +4 -1
  219. package/types/typedef.d.ts +48 -23
@@ -0,0 +1,278 @@
1
+ /**
2
+ * @fileoverview Stateless block boundary resolver for block handle positioning.
3
+ * Maps any DOM node inside the editor to its owning block-level element.
4
+ */
5
+
6
+ import { isWysiwygFrame, isTableCell, isComponentContainer } from '../../../helper/dom/domCheck';
7
+
8
+ /**
9
+ * @typedef {Object} BlockInfo
10
+ * @property {HTMLElement} element - The resolved block-level DOM element
11
+ * @property {string} type - Block type: 'p', 'heading', 'list', 'blockquote', 'pre', 'figure', 'table', 'div'
12
+ * @property {number} depth - Nesting level from wysiwyg root (0 = top-level)
13
+ * @property {HTMLElement|null} parent - Parent block element, or null if top-level
14
+ * @property {{ prev: HTMLElement|null, next: HTMLElement|null }} siblings - Adjacent blocks at same depth
15
+ * @property {DOMRect} rect - getBoundingClientRect() of the element (caller handles scroll/iframe offset)
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} FormatAPI
20
+ * @property {(node: Node, validation?: (current: *) => boolean) => HTMLElement|null} getLine
21
+ * @property {(element: Node, validation?: (current: *) => boolean) => HTMLElement|null} getBlock
22
+ * @property {(element: Node) => boolean} isLine
23
+ * @property {(element: Node) => boolean} isBlock
24
+ */
25
+
26
+ const HEADING_RE = /^H[1-6]$/;
27
+ const LIST_RE = /^(UL|OL)$/;
28
+ const LIST_ITEM_RE = /^LI$/;
29
+ const TABLE_INNER_RE = /^(THEAD|TBODY|TR|TD|TH)$/;
30
+
31
+ /**
32
+ * @description Classify the block type from a resolved element.
33
+ * @param {HTMLElement} el
34
+ * @returns {string}
35
+ */
36
+ function classifyType(el) {
37
+ const tag = el.nodeName;
38
+ if (tag === 'P' || tag === 'DIV') return 'p';
39
+ if (HEADING_RE.test(tag)) return 'heading';
40
+ if (tag === 'LI') return 'list-item';
41
+ if (LIST_RE.test(tag)) return 'list';
42
+ if (tag === 'BLOCKQUOTE') return 'blockquote';
43
+ if (tag === 'PRE') return 'pre';
44
+ if (tag === 'FIGURE') return 'figure';
45
+ if (tag === 'TABLE') return 'table';
46
+ return 'p'; // fallback for custom format elements
47
+ }
48
+
49
+ /**
50
+ * @description Walk up from a table-internal node (td, th, tr, thead, tbody) to the table element.
51
+ * @param {Node} node
52
+ * @returns {HTMLElement|null}
53
+ */
54
+ function resolveToTable(node) {
55
+ let el = node;
56
+ while (el && !isWysiwygFrame(el)) {
57
+ if (el.nodeName === 'TABLE') return /** @type {HTMLElement} */ (el);
58
+ el = el.parentNode;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * @description Check if the node is inside or is a component (.se-component, .se-flex-component)
65
+ * @param {Node} node
66
+ * @returns {boolean}
67
+ */
68
+ function isInsideComponent(node) {
69
+ let el = node;
70
+ while (el && !isWysiwygFrame(el)) {
71
+ if (el.nodeType === 1 && isComponentContainer(/** @type {Element} */ (el))) return true;
72
+ el = el.parentNode;
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * @description Count block-level ancestors between element and wysiwyg root.
79
+ * @param {HTMLElement} element
80
+ * @param {FormatAPI} format
81
+ * @returns {{ depth: number, parent: HTMLElement|null }}
82
+ */
83
+ function computeDepth(element, format) {
84
+ let depth = 0;
85
+ let parent = null;
86
+ let node = element.parentNode;
87
+
88
+ while (node && !isWysiwygFrame(node)) {
89
+ if (node.nodeType === 1 && (format.isBlock(node) || format.isLine(node))) {
90
+ if (!parent) parent = /** @type {HTMLElement} */ (node);
91
+ depth++;
92
+ }
93
+ node = node.parentNode;
94
+ }
95
+
96
+ return { depth, parent };
97
+ }
98
+
99
+ /**
100
+ * @description Find the previous and next sibling block elements at the same level.
101
+ * @param {HTMLElement} element
102
+ * @returns {{ prev: HTMLElement|null, next: HTMLElement|null }}
103
+ */
104
+ function computeSiblings(element) {
105
+ const prev = element.previousElementSibling;
106
+ const next = element.nextElementSibling;
107
+ return {
108
+ prev: /** @type {HTMLElement|null} */ (prev),
109
+ next: /** @type {HTMLElement|null} */ (next),
110
+ };
111
+ }
112
+
113
+ /**
114
+ * @description For a UL/OL, find the closest direct child LI by mouse Y.
115
+ * Recurses into the matched LI for deeper nesting.
116
+ * @param {HTMLElement} list - UL or OL element
117
+ * @param {number} mouseY
118
+ * @returns {HTMLElement|null}
119
+ */
120
+ function resolveListChildLI(list, mouseY) {
121
+ const children = list.children;
122
+ let closest = null;
123
+ let closestDist = Infinity;
124
+ for (let i = 0; i < children.length; i++) {
125
+ const child = children[i];
126
+ if (!LIST_ITEM_RE.test(child.nodeName)) continue;
127
+ const r = child.getBoundingClientRect();
128
+ if (mouseY >= r.top && mouseY <= r.bottom) {
129
+ // Exact hit — recurse for deeper nesting
130
+ return resolveNestedLI(/** @type {HTMLElement} */ (child), mouseY) || /** @type {HTMLElement} */ (child);
131
+ }
132
+ const dist = Math.min(Math.abs(mouseY - r.top), Math.abs(mouseY - r.bottom));
133
+ if (dist < closestDist) {
134
+ closestDist = dist;
135
+ closest = /** @type {HTMLElement} */ (child);
136
+ }
137
+ }
138
+ return closest;
139
+ }
140
+
141
+ /**
142
+ * @description For an LI with nested sub-lists, find the closest child LI by mouse Y.
143
+ * Only resolves to a child when mouseY falls within the nested list region.
144
+ * @param {HTMLElement} li
145
+ * @param {number} mouseY
146
+ * @returns {HTMLElement|null}
147
+ */
148
+ function resolveNestedLI(li, mouseY) {
149
+ const nestedList = li.querySelector(':scope > ul, :scope > ol');
150
+ if (!nestedList) return null;
151
+
152
+ const nestedRect = nestedList.getBoundingClientRect();
153
+ // Mouse is above the nested list → stays on parent LI's own content
154
+ if (mouseY < nestedRect.top) return null;
155
+
156
+ const childLIs = nestedList.children;
157
+ let closest = null;
158
+ let closestDist = Infinity;
159
+ for (let i = 0; i < childLIs.length; i++) {
160
+ const child = childLIs[i];
161
+ if (!LIST_ITEM_RE.test(child.nodeName)) continue;
162
+ const r = child.getBoundingClientRect();
163
+ if (mouseY >= r.top && mouseY <= r.bottom) {
164
+ // Exact hit — recurse for deeper nesting
165
+ return resolveNestedLI(/** @type {HTMLElement} */ (child), mouseY) || /** @type {HTMLElement} */ (child);
166
+ }
167
+ const dist = Math.min(Math.abs(mouseY - r.top), Math.abs(mouseY - r.bottom));
168
+ if (dist < closestDist) {
169
+ closestDist = dist;
170
+ closest = /** @type {HTMLElement} */ (child);
171
+ }
172
+ }
173
+ return closest;
174
+ }
175
+
176
+ /**
177
+ * @description Resolve any DOM node inside the editor to its owning block-level element.
178
+ * @param {Node|null} node - Any DOM node inside the editor
179
+ * @param {FormatAPI} format - Injected format methods (getLine, getBlock, isLine, isBlock)
180
+ * @param {HTMLElement} wysiwygFrame - The wysiwyg root element
181
+ * @param {number} [mouseY] - Mouse clientY for nested list resolution
182
+ * @returns {BlockInfo|null} Block information, or null if node is outside editable area
183
+ */
184
+ export function resolveBlock(node, format, wysiwygFrame, mouseY) {
185
+ if (!node || !wysiwygFrame) return null;
186
+
187
+ // Text node → use parent element
188
+ if (node.nodeType === 3) {
189
+ node = node.parentNode;
190
+ if (!node) return null;
191
+ }
192
+
193
+ // Already at wysiwyg root
194
+ if (isWysiwygFrame(node)) return null;
195
+
196
+ // Skip components (images, videos, etc.) — they have their own interaction
197
+ if (isInsideComponent(node)) return null;
198
+
199
+ let resolved = null;
200
+
201
+ // Table cell or table-internal → resolve to TABLE
202
+ if (TABLE_INNER_RE.test(node.nodeName)) {
203
+ resolved = resolveToTable(node);
204
+ }
205
+ // List container (UL/OL) → keep as-is, resolveListChildLI will pick the right LI
206
+ else if (LIST_RE.test(node.nodeName)) {
207
+ resolved = /** @type {HTMLElement} */ (node);
208
+ }
209
+ // List item → resolve to LI itself (each list item gets its own handle)
210
+ else if (LIST_ITEM_RE.test(node.nodeName)) {
211
+ resolved = /** @type {HTMLElement} */ (node);
212
+ }
213
+ // Check if inside a table cell — walk up to table
214
+ else if (isTableCell(node.parentNode)) {
215
+ resolved = resolveToTable(node);
216
+ } else {
217
+ // Try getLine first (finds P, H1-H6, PRE, LI, etc.)
218
+ const line = format.getLine(node, null);
219
+ if (line) {
220
+ resolved = line;
221
+ }
222
+
223
+ // If no line found, try getBlock (finds BLOCKQUOTE, UL, OL, TABLE, etc.)
224
+ if (!resolved) {
225
+ const block = format.getBlock(node, null);
226
+ if (block) {
227
+ // For table internals, resolve up to TABLE
228
+ if (TABLE_INNER_RE.test(block.nodeName)) {
229
+ resolved = resolveToTable(block);
230
+ } else {
231
+ resolved = block;
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ // If nothing resolved, try walking up manually to find any block-level element
238
+ if (!resolved) {
239
+ let el = /** @type {Node} */ (node);
240
+ while (el && !isWysiwygFrame(el)) {
241
+ if (el.nodeType === 1 && el.parentNode && isWysiwygFrame(el.parentNode)) {
242
+ resolved = /** @type {HTMLElement} */ (el);
243
+ break;
244
+ }
245
+ el = el.parentNode;
246
+ }
247
+ }
248
+
249
+ if (!resolved) return null;
250
+
251
+ // Final component check on resolved element
252
+ if (isInsideComponent(resolved)) return null;
253
+
254
+ // For UL/OL, resolve to the closest child LI by mouse Y.
255
+ // For LI with nested sub-lists, find the deepest child LI.
256
+ if (mouseY !== undefined) {
257
+ if (LIST_RE.test(resolved.nodeName)) {
258
+ const childLI = resolveListChildLI(/** @type {HTMLElement} */ (resolved), mouseY);
259
+ if (childLI) resolved = childLI;
260
+ } else if (LIST_ITEM_RE.test(resolved.nodeName)) {
261
+ const nested = resolveNestedLI(/** @type {HTMLElement} */ (resolved), mouseY);
262
+ if (nested) resolved = nested;
263
+ }
264
+ }
265
+
266
+ const { depth, parent } = computeDepth(resolved, format);
267
+ const siblings = computeSiblings(resolved);
268
+ const type = classifyType(resolved);
269
+
270
+ return {
271
+ element: resolved,
272
+ type,
273
+ depth,
274
+ parent,
275
+ siblings,
276
+ rect: resolved.getBoundingClientRect(),
277
+ };
278
+ }
@@ -54,7 +54,8 @@ class Finder {
54
54
  Finder.#highlightStyleInjected = true;
55
55
  const style = _d.createElement('style');
56
56
  style.textContent =
57
- '::highlight(se-find-match){background-color:var(--se-find-match-color,rgba(255,213,0,.4));color:inherit}' + '::highlight(se-find-current){background-color:var(--se-find-current-color,rgba(255,150,50,.7));color:inherit}';
57
+ '::highlight(se-find-match){background-color:var(--se-find-match-color,rgba(255,213,0,.4));color:inherit}' +
58
+ '::highlight(se-find-current){background-color:var(--se-find-current-color,rgba(255,150,50,.7));color:inherit}';
58
59
  _d.head.appendChild(style);
59
60
  }
60
61
 
@@ -99,7 +100,9 @@ class Finder {
99
100
 
100
101
  // Update sticky top (responsive resize, more layer, etc.)
101
102
  if (env.isResizeObserverSupported) {
102
- this.#resizeObserver = new ResizeObserver(() => this.#updateStickyTop()).observe(this.#$.context.get('toolbar_main'));
103
+ this.#resizeObserver = new ResizeObserver(() => this.#updateStickyTop()).observe(
104
+ this.#$.context.get('toolbar_main'),
105
+ );
103
106
  }
104
107
  }
105
108
  }
@@ -157,9 +160,11 @@ class Finder {
157
160
  const stickyTop = this.#$.options.get('_toolbar_sticky');
158
161
  if (this.#store.mode.isBottom) {
159
162
  this.#panel.style.top = 'auto';
160
- this.#panel.style.bottom = stickyTop >= 0 ? stickyTop + this.#$.context.get('toolbar_main').offsetHeight + 'px' : '0px';
163
+ this.#panel.style.bottom =
164
+ stickyTop >= 0 ? stickyTop + this.#$.context.get('toolbar_main').offsetHeight + 'px' : '0px';
161
165
  } else {
162
- this.#panel.style.top = stickyTop >= 0 ? stickyTop + this.#$.context.get('toolbar_main').offsetHeight + 'px' : '0px';
166
+ this.#panel.style.top =
167
+ stickyTop >= 0 ? stickyTop + this.#$.context.get('toolbar_main').offsetHeight + 'px' : '0px';
163
168
  }
164
169
  }
165
170
 
@@ -753,7 +758,9 @@ class Finder {
753
758
  // Mark fallback: update active mark
754
759
  this.#updateCurrentMarkHighlight();
755
760
 
756
- const currentMark = this.#markElements.find((m) => m.getAttribute('data-se-find-idx') === String(this.#currentIndex));
761
+ const currentMark = this.#markElements.find(
762
+ (m) => m.getAttribute('data-se-find-idx') === String(this.#currentIndex),
763
+ );
757
764
  if (currentMark) {
758
765
  this.#$.selection.scrollTo(currentMark, { behavior: 'auto', noFocus: true });
759
766
  }
@@ -780,7 +787,10 @@ class Finder {
780
787
  const freshMatches = this.#findAllMatches(this.#searchTerm, wysiwyg);
781
788
  if (this.#currentIndex >= freshMatches.length) return;
782
789
 
783
- this.#replaceRange(freshMatches[this.#currentIndex], replaceText ?? (this.#replaceInput ? this.#replaceInput.value : ''));
790
+ this.#replaceRange(
791
+ freshMatches[this.#currentIndex],
792
+ replaceText ?? (this.#replaceInput ? this.#replaceInput.value : ''),
793
+ );
784
794
  this.#$.history.push(false);
785
795
 
786
796
  // Re-search
@@ -43,6 +43,8 @@ class Menu {
43
43
  // members
44
44
  /** @type {Object<string, HTMLElement>} */
45
45
  this.targetMap = {};
46
+ /** @type {Object<string, Array<DropdownItem>>} Structured items keyed by plugin name */
47
+ this.itemsMap = {};
46
48
  this.index = -1;
47
49
  this.menus = [];
48
50
  // dropdown
@@ -79,19 +81,37 @@ class Menu {
79
81
  * @description Method for managing dropdown element.
80
82
  * - You must add the `dropdown` element using this method at custom plugin.
81
83
  * @param {{key: string, type: string}} classObj Class object
82
- * @param {Node} menu Dropdown element
84
+ * @param {Node|Array<DropdownItem>} menuOrItems Dropdown element or array of standardized items
85
+ * @param {Object} [options] Options when passing items array
86
+ * @param {string} [options.className] Additional CSS class for the wrapper element
87
+ * @param {string} [options.prependHTML] HTML to prepend inside the `<ul>` (e.g. default/sub-list items)
88
+ * @returns {HTMLElement} The registered menu element
83
89
  */
84
- initDropdownTarget({ key, type }, menu) {
85
- if (key) {
86
- if (!IsFree(type)) {
87
- /** @type {HTMLElement} */ (menu).setAttribute('data-key', key);
88
- this.#dropdownCommands.push(key);
90
+ initDropdownTarget({ key, type }, menuOrItems, options) {
91
+ if (!key) throw Error("[SUNEDITOR.init.fail] The plugin's key is not added.");
92
+
93
+ let menu;
94
+ if (Array.isArray(menuOrItems)) {
95
+ menu = CreateDropdownMenu(menuOrItems, options);
96
+
97
+ const allButtons = menu.querySelectorAll('.se-list-inner li > button');
98
+ const offset = allButtons.length - menuOrItems.length;
99
+ for (let i = 0; i < menuOrItems.length; i++) {
100
+ menuOrItems[i]._element = /** @type {HTMLElement} */ (allButtons[offset + i]) || null;
89
101
  }
90
- this.#context.get('menuTray').appendChild(menu);
91
- this.targetMap[key] = /** @type {HTMLElement} */ (menu);
102
+ this.itemsMap[key] = menuOrItems;
92
103
  } else {
93
- throw Error("[SUNEDITOR.init.fail] The plugin's key is not added.");
104
+ menu = /** @type {HTMLElement} */ (menuOrItems);
105
+ }
106
+
107
+ if (!IsFree(type)) {
108
+ menu.setAttribute('data-key', key);
109
+ this.#dropdownCommands.push(key);
94
110
  }
111
+
112
+ this.#context.get('menuTray').appendChild(menu);
113
+ this.targetMap[key] = menu;
114
+ return menu;
95
115
  }
96
116
 
97
117
  /**
@@ -102,7 +122,9 @@ class Menu {
102
122
  this.#removeGlobalEvent();
103
123
  const moreBtn = this.#checkMoreLayer(button);
104
124
  if (moreBtn) {
105
- const target = dom.query.getParentElement(moreBtn, '.se-btn-tray').querySelector('[data-command="' + moreBtn.getAttribute('data-ref') + '"]');
125
+ const target = dom.query
126
+ .getParentElement(moreBtn, '.se-btn-tray')
127
+ .querySelector('[data-command="' + moreBtn.getAttribute('data-ref') + '"]');
106
128
  if (target) {
107
129
  this.#$.commandDispatcher.runFromTarget(target);
108
130
  this.dropdownOn(button);
@@ -123,13 +145,31 @@ class Menu {
123
145
  }
124
146
 
125
147
  this.#context.get('menuTray').showPopover?.();
126
- this.#bindClose_dropdown_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.mousedown, false);
148
+ this.#bindClose_dropdown_mouse = this.#eventManager.addGlobalEvent(
149
+ 'mousedown',
150
+ this.#globalEventHandler.mousedown,
151
+ false,
152
+ );
127
153
  if (this.#dropdownCommands.includes(dropdownName)) {
128
154
  this.menus = converter.nodeListToArray(menu.querySelectorAll('[data-command]'));
129
155
  if (this.menus.length > 0) {
130
- this.#bindClose_dropdown_key = this.#eventManager.addGlobalEvent('keydown', this.#globalEventHandler.keydown, false);
131
- this.#bindMenu_mousemove = this.#eventManager.addEvent(menu, 'mousemove', this.#globalEventHandler.mousemove, false);
132
- this.#bindMenu_mouseout = this.#eventManager.addEvent(menu, 'mouseout', this.#globalEventHandler.mouseout, false);
156
+ this.#bindClose_dropdown_key = this.#eventManager.addGlobalEvent(
157
+ 'keydown',
158
+ this.#globalEventHandler.keydown,
159
+ false,
160
+ );
161
+ this.#bindMenu_mousemove = this.#eventManager.addEvent(
162
+ menu,
163
+ 'mousemove',
164
+ this.#globalEventHandler.mousemove,
165
+ false,
166
+ );
167
+ this.#bindMenu_mouseout = this.#eventManager.addEvent(
168
+ menu,
169
+ 'mouseout',
170
+ this.#globalEventHandler.mouseout,
171
+ false,
172
+ );
133
173
  }
134
174
  }
135
175
 
@@ -202,7 +242,8 @@ class Menu {
202
242
  this.#removeGlobalEvent();
203
243
 
204
244
  this.currentContainerActiveButton = /** @type {HTMLButtonElement} */ (button);
205
- const containerName = (this.currentContainerName = this.currentContainerActiveButton.getAttribute('data-command'));
245
+ const containerName = (this.currentContainerName =
246
+ this.currentContainerActiveButton.getAttribute('data-command'));
206
247
  this.currentContainer = this.targetMap[containerName];
207
248
 
208
249
  if (isMobile) {
@@ -212,7 +253,11 @@ class Menu {
212
253
  }
213
254
 
214
255
  this.#context.get('menuTray').showPopover?.();
215
- this.#bindClose_cons_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.containerDown, false);
256
+ this.#bindClose_cons_mouse = this.#eventManager.addGlobalEvent(
257
+ 'mousedown',
258
+ this.#globalEventHandler.containerDown,
259
+ false,
260
+ );
216
261
 
217
262
  if (this.#$.plugins[containerName].on) this.#$.plugins[containerName].on(button);
218
263
  this.#store.set('_preventBlur', true);
@@ -245,7 +290,13 @@ class Menu {
245
290
  * @param {HTMLElement} menu Menu element
246
291
  */
247
292
  __resetMenuPosition(element, menu) {
248
- this.#$.offset.setRelPosition(menu, this.#contextProvider.carrierWrapper, element.parentElement, dom.query.getParentElement(element, '.se-toolbar'), { preferUp: this.#store.mode.isBottom });
293
+ this.#$.offset.setRelPosition(
294
+ menu,
295
+ this.#contextProvider.carrierWrapper,
296
+ element.parentElement,
297
+ dom.query.getParentElement(element, '.se-toolbar'),
298
+ { preferUp: this.#store.mode.isBottom },
299
+ );
249
300
  }
250
301
 
251
302
  /**
@@ -270,7 +321,13 @@ class Menu {
270
321
  menu.style.height = '';
271
322
  dom.utils.addClass(element.parentElement.children, 'on');
272
323
 
273
- this.#$.offset.setRelPosition(menu, this.#contextProvider.carrierWrapper, element.parentElement, dom.query.getParentElement(element, '.se-toolbar'), { preferUp: this.#store.mode.isBottom });
324
+ this.#$.offset.setRelPosition(
325
+ menu,
326
+ this.#contextProvider.carrierWrapper,
327
+ element.parentElement,
328
+ dom.query.getParentElement(element, '.se-toolbar'),
329
+ { preferUp: this.#store.mode.isBottom },
330
+ );
274
331
 
275
332
  menu.style.visibility = '';
276
333
 
@@ -463,4 +520,48 @@ function IsFree(type) {
463
520
  return /free$/.test(type);
464
521
  }
465
522
 
523
+ /**
524
+ * @typedef {Object} DropdownItem
525
+ * @property {string} command - `data-command` attribute value
526
+ * @property {string} [value] - `data-value` attribute value
527
+ * @property {string} title - Title text (used for `title` and `aria-label`)
528
+ * @property {string} innerHTML - Button inner HTML content
529
+ * @property {string} [className] - Additional CSS class for the `<button>`
530
+ * @property {Object<string, string>} [attrs] - Extra data attributes (e.g. `{ 'data-class': 'xxx' }`)
531
+ * @property {HTMLElement} [_element] - Rendered button element (set by `initDropdownTarget`)
532
+ */
533
+
534
+ /**
535
+ * @description Creates a dropdown menu element from standardized item objects.
536
+ * @param {Array<DropdownItem>} items - Menu items
537
+ * @param {Object} [options]
538
+ * @param {string} [options.className] - Additional class for the wrapper `<div>`
539
+ * @param {string} [options.prependHTML] - HTML to prepend inside the `<ul>` (e.g. default value items)
540
+ * @returns {HTMLElement}
541
+ */
542
+ function CreateDropdownMenu(items, options = {}) {
543
+ let html = '';
544
+ for (let i = 0, len = items.length; i < len; i++) {
545
+ const item = items[i];
546
+ let attrStr = '';
547
+ if (item.attrs) {
548
+ const keys = Object.keys(item.attrs);
549
+ for (let a = 0; a < keys.length; a++) {
550
+ attrStr += ` ${keys[a]}="${item.attrs[keys[a]]}"`;
551
+ }
552
+ }
553
+ html += /*html*/ `
554
+ <li>
555
+ <button type="button" class="se-btn se-btn-list${item.className ? ' ' + item.className : ''}" data-command="${item.command}"${item.value !== undefined ? ` data-value="${item.value}"` : ''} title="${item.title}" aria-label="${item.title}"${attrStr}>${item.innerHTML}</button>
556
+ </li>`;
557
+ }
558
+
559
+ return dom.utils.createElement(
560
+ 'DIV',
561
+ { class: 'se-dropdown se-list-layer' + (options.className ? ' ' + options.className : '') },
562
+ /*html*/ `<div class="se-list-inner"><ul class="se-list-basic">${options.prependHTML || ''}${html}</ul></div>`,
563
+ );
564
+ }
565
+
566
+ export { CreateDropdownMenu };
466
567
  export default Menu;