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
@@ -75,7 +75,14 @@ class FileManager {
75
75
  this.uploadFileLength = data.size;
76
76
  }
77
77
 
78
- this.apiManager.call({ method: 'POST', url: uploadUrl, headers: uploadHeader, data: formData, callBack, errorCallBack });
78
+ this.apiManager.call({
79
+ method: 'POST',
80
+ url: uploadUrl,
81
+ headers: uploadHeader,
82
+ data: formData,
83
+ callBack,
84
+ errorCallBack,
85
+ });
79
86
  }
80
87
 
81
88
  /**
@@ -105,7 +112,12 @@ class FileManager {
105
112
  this.uploadFileLength = data.size;
106
113
  }
107
114
 
108
- return await this.apiManager.asyncCall({ method: 'POST', url: uploadUrl, headers: uploadHeader, data: formData });
115
+ return await this.apiManager.asyncCall({
116
+ method: 'POST',
117
+ url: uploadUrl,
118
+ headers: uploadHeader,
119
+ data: formData,
120
+ });
109
121
  }
110
122
 
111
123
  /**
@@ -196,7 +208,14 @@ class FileManager {
196
208
 
197
209
  infoList.splice(i, 1);
198
210
 
199
- const params = { element: null, index: dataIndex, state: 'delete', info: null, remainingFilesCount: 0, pluginName: this.kind };
211
+ const params = {
212
+ element: null,
213
+ index: dataIndex,
214
+ state: 'delete',
215
+ info: null,
216
+ remainingFilesCount: 0,
217
+ pluginName: this.kind,
218
+ };
200
219
  if (this.actionEventName) {
201
220
  this.#$.eventManager.triggerEvent(this.actionEventName, params);
202
221
  }
@@ -214,9 +233,17 @@ class FileManager {
214
233
  const params = { element: null, state: 'delete', info: null, remainingFilesCount: 0, pluginName: this.kind };
215
234
  for (let i = 0, len = this.infoList.length; i < len; i++) {
216
235
  if (this.actionEventName) {
217
- this.#$.eventManager.triggerEvent(this.actionEventName, { ...params, index: this.infoList[i].index, pluginName: this.kind });
236
+ this.#$.eventManager.triggerEvent(this.actionEventName, {
237
+ ...params,
238
+ index: this.infoList[i].index,
239
+ pluginName: this.kind,
240
+ });
218
241
  }
219
- this.#$.eventManager.triggerEvent('onFileManagerAction', { ...params, index: this.infoList[i].index, pluginName: this.kind });
242
+ this.#$.eventManager.triggerEvent('onFileManagerAction', {
243
+ ...params,
244
+ index: this.infoList[i].index,
245
+ pluginName: this.kind,
246
+ });
220
247
  }
221
248
 
222
249
  this.infoList = [];
@@ -233,7 +260,14 @@ class FileManager {
233
260
  if (index === this.infoList[i].index) {
234
261
  this.infoList.splice(i, 1);
235
262
  if (this.actionEventName) {
236
- this.#$.eventManager.triggerEvent(this.actionEventName, { element: null, index, state: 'delete', info: null, remainingFilesCount: 0, pluginName: this.kind });
263
+ this.#$.eventManager.triggerEvent(this.actionEventName, {
264
+ element: null,
265
+ index,
266
+ state: 'delete',
267
+ info: null,
268
+ remainingFilesCount: 0,
269
+ pluginName: this.kind,
270
+ });
237
271
  }
238
272
  return;
239
273
  }
@@ -252,7 +286,8 @@ class FileManager {
252
286
  let state = '';
253
287
 
254
288
  file ||= {
255
- name: GetAttr(element, 'file-name') || (typeof element.src === 'string' ? element.src.split('/').pop() : ''),
289
+ name:
290
+ GetAttr(element, 'file-name') || (typeof element.src === 'string' ? element.src.split('/').pop() : ''),
256
291
  size: Number(GetAttr(element, 'file-size')) || 0,
257
292
  };
258
293
 
@@ -316,7 +351,14 @@ class FileManager {
316
351
  }
317
352
  }.bind(this, this.#$.component, element);
318
353
 
319
- const params = { element, index: dataIndex, state, info, remainingFilesCount: --this.uploadFileLength < 0 ? 0 : this.uploadFileLength, pluginName: this.kind };
354
+ const params = {
355
+ element,
356
+ index: dataIndex,
357
+ state,
358
+ info,
359
+ remainingFilesCount: --this.uploadFileLength < 0 ? 0 : this.uploadFileLength,
360
+ pluginName: this.kind,
361
+ };
320
362
  if (this.actionEventName) {
321
363
  this.#$.eventManager.triggerEvent(this.actionEventName, params);
322
364
  }
@@ -0,0 +1,597 @@
1
+ import { dom, env } from '../../helper';
2
+ import SelectMenu from './SelectMenu.js';
3
+
4
+ const { _w } = env;
5
+
6
+ /**
7
+ * @description Resolve an icon spec to an HTML string. Accepts a key from `$.icons`,
8
+ * a raw HTML snippet (anything starting with `<`), or empty.
9
+ * @param {string} icon
10
+ * @param {Object} icons
11
+ * @returns {string}
12
+ */
13
+ export function resolveIconHTML(icon, icons) {
14
+ if (!icon || typeof icon !== 'string') return '';
15
+ if (icon.charAt(0) === '<') return icon;
16
+ return icons[icon] || '';
17
+ }
18
+
19
+ /**
20
+ * @description Build the canonical command-menu row HTML. Single source of truth for the
21
+ * BlockHandle action menu and the SlashCommand menu, so they look identical.
22
+ * @param {string} label
23
+ * @param {string} iconHTML
24
+ * @returns {string}
25
+ */
26
+ export function buildRowHTML(label, iconHTML) {
27
+ const inner = iconHTML
28
+ ? `<span class="se-block-menu-icon">${iconHTML}</span><span class="se-block-menu-label">${label}</span>`
29
+ : `<span class="se-block-menu-label se-block-menu-label-full">${label}</span>`;
30
+ return `<button type="button" class="se-cmd-row">${inner}</button>`;
31
+ }
32
+
33
+ /**
34
+ * @typedef {Object} CommandMenuItem
35
+ * @property {string} [key] - Optional identifier.
36
+ * @property {string} title - Display label.
37
+ * @property {string} [icon] - `$.icons` key or raw HTML snippet.
38
+ * @property {string[]} [keywords] - Extra search terms (used by the filter).
39
+ * @property {function(SunEditor.Deps, *): void | Promise<void>} action - Custom callback. The second
40
+ * argument is the host-supplied context (e.g. `{block}` for BlockHandle, `{triggerChar, query}` for SlashCommand).
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} SubmenuChild
45
+ * @property {string} pluginName - Owning plugin key.
46
+ * @property {HTMLElement} element - The original toolbar dropdown button element. Passed to `plugin.action(element)` on click.
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} ResolvedItem
51
+ * @property {'custom'|'plugin'|'submenu'|'dropdownFree'} kind
52
+ * @property {string} title
53
+ * @property {string} iconHTML
54
+ * @property {string} search - Lower-cased haystack for substring filtering.
55
+ * @property {CommandMenuItem} [raw]
56
+ * @property {string} [name] - Plugin key.
57
+ * @property {string} [type] - Plugin type (`'modal'`, `'command'`, etc.).
58
+ * @property {string} [command]
59
+ * @property {SubmenuChild[]} [children] - For `kind: 'submenu'` — clickable child rows (one per dropdown item).
60
+ * @property {string[]} [childMenus] - For `kind: 'submenu'` — HTML for each child row.
61
+ */
62
+
63
+ /**
64
+ * @typedef {Object} CommandMenuParams
65
+ * @property {Array<string | CommandMenuItem>} items - Raw menu entries.
66
+ * @property {function(string, Object, *, Object, Object): ({title: string, icon: string, type: string, command: string} | null)} resolveButton
67
+ * Caller-provided plugin/button resolver. Inject `ResolveButton` from `core/section/constructor`.
68
+ * Injected (not imported) because this module lives under `src/modules/` and cannot import from `src/core/*`.
69
+ * @property {Object} selectMenuParams - Base SelectMenu params (`position`, `minWidth`, `keydownTarget`, etc.).
70
+ * @property {function(CommandMenuItem, { icons: Object }): string} [renderCustomItem] - Optional renderer
71
+ * applied to custom (object) items only. Plugin-string items always render with `buildRowHTML`.
72
+ */
73
+
74
+ /**
75
+ * @class
76
+ * @description Shared command-menu module used by BlockHandle and SlashCommand.
77
+ * - Owns the underlying `SelectMenu`, renders rows with the canonical BlockHandle row HTML.
78
+ * - Supports four item kinds:
79
+ * - `'custom'` — user-defined `{title, icon?, action}` invoked with `($, hostContext)`.
80
+ * - `'plugin'` — plain plugin (modal/browser/command/popup) dispatched via `plugin.open/show/action`.
81
+ * - `'submenu'` — dropdown plugin auto-expanded inline (children come from `$.menu.itemsMap`).
82
+ * - `'dropdownFree'` — dropdown-free plugin (e.g. `fontColor`, `table`) shown with a submenu arrow
83
+ * and an attached hover flyout that reuses the plugin's toolbar dropdown DOM.
84
+ * - Handles blur prevention on mousedown, hover flyout lifecycle, and click-to-toggle for dropdown-free.
85
+ *
86
+ * Host responsibilities (kept outside this module):
87
+ * - Pre-dispatch side effects (BlockHandle: `expandRangeToFullLines`; SlashCommand: delete trigger text).
88
+ * - Anchor element selection (BlockHandle anchors to the drag button; SlashCommand to a Controller wrapper).
89
+ * - Context object for custom-item actions.
90
+ */
91
+ class CommandMenu {
92
+ #$;
93
+ #resolveButton;
94
+ #rawItems;
95
+ #resolved = null;
96
+ #renderCustomItem;
97
+
98
+ /** @type {Map<number, { name: string, plugin: any, li: HTMLElement }>} */
99
+ #freeMap = new Map();
100
+ /** @type {?{ dropdown: HTMLElement, plugin: any, originalParent: ?Node, anchorLi: HTMLElement, evClick: ?SunEditor.Event.Info }} */
101
+ #flyoutState = null;
102
+
103
+ /**
104
+ * @type {Array<{ name: string, idx: number }>}
105
+ */
106
+ #pendingTargets = [];
107
+
108
+ /**
109
+ * @type {Array<{ name: string, target: HTMLElement }>}
110
+ */
111
+ #registered = [];
112
+
113
+ /** @type {SelectMenu} */
114
+ selectMenu;
115
+
116
+ /**
117
+ * @constructor
118
+ * @param {*} _host - Reserved for future host-aware behavior (kept for API symmetry with `Modal`/`Controller`).
119
+ * @param {SunEditor.Deps} $ - Deps bag.
120
+ * @param {CommandMenuParams} params
121
+ */
122
+ constructor(_host, $, params) {
123
+ this.#$ = $;
124
+ this.#resolveButton = params.resolveButton;
125
+ this.#rawItems = Array.isArray(params.items) ? params.items : [];
126
+ this.#renderCustomItem = typeof params.renderCustomItem === 'function' ? params.renderCustomItem : null;
127
+
128
+ // Wrap the host's closeMethod so the flyout is always torn down with the menu.
129
+ const userClose = params.selectMenuParams?.closeMethod;
130
+ this.selectMenu = new SelectMenu($, {
131
+ ...params.selectMenuParams,
132
+ closeMethod: () => {
133
+ this.#closeFlyout();
134
+ this.#unregisterAll();
135
+ userClose?.();
136
+ },
137
+ subEscMethod: () => {
138
+ if (!this.#flyoutState) return false;
139
+ const anchorLi = this.#flyoutState.anchorLi;
140
+ this.#closeFlyout();
141
+ const idx = anchorLi ? Number(anchorLi.getAttribute('data-index')) : NaN;
142
+ if (!Number.isNaN(idx)) this.selectMenu.setItem(idx);
143
+ return true;
144
+ },
145
+ });
146
+ }
147
+
148
+ /**
149
+ * @description Bind the menu to a reference element and register the click handler.
150
+ * @param {HTMLElement} referElement
151
+ * @param {(item: ResolvedItem|null) => void} onSelect - Called with the resolved item picked by the user.
152
+ * Hosts run their pre-dispatch hook here (delete trigger, etc.) and then call `dispatch(item, ctx)`.
153
+ * @param {{ class?: string, style?: string }} [attr]
154
+ */
155
+ attach(referElement, onSelect, attr = {}) {
156
+ this.selectMenu.on(referElement, /** @type {*} */ (onSelect), attr);
157
+
158
+ this.#$.eventManager.addEvent(this.selectMenu.form, 'mousedown', (e) => {
159
+ if (env.isMobile) {
160
+ this.#$.store.set('_preventBlur', true);
161
+ } else {
162
+ e.preventDefault();
163
+ }
164
+ });
165
+
166
+ this.#$.eventManager.addEvent(this.selectMenu.form, 'mousemove', this.#onMenuMouseMove.bind(this));
167
+ }
168
+
169
+ /**
170
+ * @description Replace the items list (e.g. when host options change). Invalidates the resolved cache.
171
+ * @param {Array<string | CommandMenuItem>} items
172
+ */
173
+ setItems(items) {
174
+ this.#rawItems = Array.isArray(items) ? items : [];
175
+ this.#resolved = null;
176
+ }
177
+
178
+ /**
179
+ * @description Lazily normalize raw items. Lazy because plugin instances (`$.plugins`) may not
180
+ * be available during the host's constructor.
181
+ * @returns {ResolvedItem[]}
182
+ */
183
+ getItems() {
184
+ if (this.#resolved) return this.#resolved;
185
+ /** @type {ResolvedItem[]} */
186
+ const out = [];
187
+ for (const entry of this.#rawItems) {
188
+ if (typeof entry === 'string') {
189
+ const r = this.#resolveButton(entry, this.#$.plugins, this.#$.options, this.#$.icons, this.#$.lang);
190
+ if (!r) continue;
191
+
192
+ const type = r.type || '';
193
+
194
+ // Dropdown-free
195
+ if (/dropdown-free/.test(type)) {
196
+ out.push({
197
+ kind: 'dropdownFree',
198
+ title: r.title,
199
+ iconHTML: r.icon || '',
200
+ search: (r.title + ' ' + entry).toLowerCase(),
201
+ name: entry,
202
+ type,
203
+ });
204
+ continue;
205
+ }
206
+
207
+ // Dropdown
208
+ if (/dropdown/.test(type)) {
209
+ const menuItems = this.#$.menu?.itemsMap?.[entry] || [];
210
+ if (menuItems.length > 0) {
211
+ out.push({
212
+ kind: 'submenu',
213
+ title: r.title,
214
+ iconHTML: r.icon || '',
215
+ search: (r.title + ' ' + entry).toLowerCase(),
216
+ name: entry,
217
+ children: menuItems.map((mi) => ({ pluginName: entry, element: mi._element })),
218
+ childMenus: menuItems.map((mi) => {
219
+ const iconEl =
220
+ mi._element?.querySelector('.se-list-icon') || mi._element?.querySelector('svg');
221
+ const icon = iconEl ? iconEl.outerHTML : '';
222
+ const label = icon ? `${icon}<span>${mi.title}</span>` : mi.title;
223
+ return `<span class="se-block-submenu-item">${label}</span>`;
224
+ }),
225
+ });
226
+ continue;
227
+ }
228
+ }
229
+
230
+ out.push({
231
+ kind: 'plugin',
232
+ title: r.title,
233
+ iconHTML: r.icon || '',
234
+ search: (r.title + ' ' + entry).toLowerCase(),
235
+ name: entry,
236
+ type,
237
+ command: r.command,
238
+ });
239
+ } else if (entry && typeof entry === 'object' && typeof entry.action === 'function') {
240
+ const title = entry.title || entry.key || '';
241
+ const keywords = Array.isArray(entry.keywords) ? entry.keywords.join(' ') : '';
242
+ out.push({
243
+ kind: 'custom',
244
+ title,
245
+ iconHTML: resolveIconHTML(entry.icon, this.#$.icons),
246
+ search: (title + ' ' + keywords).toLowerCase(),
247
+ raw: entry,
248
+ });
249
+ }
250
+ }
251
+ this.#resolved = out;
252
+ return out;
253
+ }
254
+
255
+ /**
256
+ * @description Case-insensitive substring filter on `title + keywords + plugin name`.
257
+ * Empty query returns all items (up to `limit`).
258
+ * @param {string} query
259
+ * @param {number} [limit=Infinity]
260
+ * @returns {ResolvedItem[]}
261
+ */
262
+ filter(query, limit = Infinity) {
263
+ const items = this.getItems();
264
+ const q = (query || '').toLowerCase().trim();
265
+ const out = [];
266
+ for (const it of items) {
267
+ if (!q || it.search.indexOf(q) !== -1) out.push(it);
268
+ if (out.length >= limit) break;
269
+ }
270
+ return out;
271
+ }
272
+
273
+ /**
274
+ * @description Build row HTML for a list of resolved items.
275
+ * @param {ResolvedItem[]} items
276
+ * @returns {string[]}
277
+ */
278
+ renderRows(items) {
279
+ const ctx = { icons: this.#$.icons };
280
+ const arrow = `<span class="se-submenu-arrow">${this.#$.icons.menu_arrow_right}</span>`;
281
+ return items.map((it) => {
282
+ if (it.kind === 'custom' && this.#renderCustomItem) return this.#renderCustomItem(it.raw, ctx);
283
+ const row = buildRowHTML(it.title, it.iconHTML);
284
+ return it.kind === 'dropdownFree' ? row + arrow : row;
285
+ });
286
+ }
287
+
288
+ /**
289
+ * @description Populate the SelectMenu rows.
290
+ * @param {ResolvedItem[]} items
291
+ */
292
+ createRows(items) {
293
+ // SlashCommand rebuilds rows on every keystroke; previous `li`s become detached. Drop their
294
+ // registrations before the SelectMenu rewrite so toolbar map never points at orphaned nodes.
295
+ this.#unregisterAll();
296
+
297
+ const selectItems = items.map((it) =>
298
+ it.kind === 'submenu' ? { children: it.children, childMenus: it.childMenus } : it,
299
+ );
300
+ this.selectMenu.create(selectItems, this.renderRows(items));
301
+
302
+ this.#freeMap.clear();
303
+ this.#pendingTargets.length = 0;
304
+ items.forEach((it, i) => {
305
+ if (it.kind === 'dropdownFree') {
306
+ this.#freeMap.set(i, {
307
+ name: it.name,
308
+ plugin: this.#$.plugins[it.name],
309
+ li: this.selectMenu.menus[i],
310
+ });
311
+ }
312
+ if (it.kind === 'plugin' || it.kind === 'dropdownFree') {
313
+ this.#pendingTargets.push({ name: it.name, idx: i });
314
+ }
315
+ });
316
+
317
+ // SlashCommand stays "open" across createRows calls — re-register immediately so the freshly
318
+ // built `li` set takes over from the discarded one without waiting for the next open().
319
+ if (this.selectMenu.isOpen) this.#registerAll();
320
+ }
321
+
322
+ /**
323
+ * @description Open the SelectMenu. Lazily (re-)registers menu rows into
324
+ * `commandDispatcher.targets` so `selectionState.update` paints the `active` class on them through
325
+ * @param {string} [position]
326
+ */
327
+ open(position) {
328
+ this.selectMenu.open(position);
329
+ if (this.#registered.length === 0) this.#registerAll();
330
+ }
331
+
332
+ /**
333
+ * @description Close the SelectMenu. Also unregisters menu rows from `commandDispatcher.targets`
334
+ */
335
+ close() {
336
+ this.#unregisterAll();
337
+ this.selectMenu.close();
338
+ }
339
+
340
+ /**
341
+ * @description Whether a sub-panel (native submenu or dropdown-free flyout) is currently open.
342
+ * Lets an owning Controller keep the menu open on ESC and close only the sub-panel.
343
+ * @returns {boolean}
344
+ */
345
+ hasOpenSubPanel() {
346
+ return this.selectMenu.hasOpenSubmenu() || !!this.#flyoutState;
347
+ }
348
+
349
+ /**
350
+ * @description Push every `#pendingTargets` row's inner `<button>` into the toolbar target map
351
+ */
352
+ #registerAll() {
353
+ const cd = this.#$.commandDispatcher;
354
+ for (const { name, idx } of this.#pendingTargets) {
355
+ const btn = this.#resolveButtonTarget(idx);
356
+ if (!btn) continue;
357
+ cd.registerTargets(name, btn);
358
+ this.#registered.push({ name, target: btn });
359
+ }
360
+ // Bust the dedup cache so applyTagEffect re-runs even when the selection node is unchanged.
361
+ this.#$.store.set('_lastSelectionNode', null);
362
+ this.#$.eventManager.applyTagEffect();
363
+ }
364
+
365
+ /**
366
+ * @description Drop every `(cmd, target)` pair this instance registered. Safe to call repeatedly.
367
+ */
368
+ #unregisterAll() {
369
+ if (this.#registered.length === 0) return;
370
+ const cd = this.#$.commandDispatcher;
371
+ for (const { name, target } of this.#registered) cd.unregisterTargets(name, target);
372
+ this.#registered.length = 0;
373
+ }
374
+
375
+ /**
376
+ * @description Resolve the registration target for menu index `idx`.
377
+ * (defensive — should always exist after {@link buildRowHTML}).
378
+ * @param {number} idx
379
+ * @returns {?HTMLButtonElement}
380
+ */
381
+ #resolveButtonTarget(idx) {
382
+ const li = this.selectMenu.menus[idx];
383
+ if (!li) return null;
384
+ return /** @type {?HTMLButtonElement} */ (li.querySelector('button.se-cmd-row') || li);
385
+ }
386
+
387
+ /**
388
+ * @description Highlight a row.
389
+ * @param {number} index
390
+ */
391
+ setItem(index) {
392
+ this.selectMenu.setItem(index);
393
+ }
394
+
395
+ /**
396
+ * @description Soft-hide / soft-show without changing open state. Pass-through.
397
+ * @param {boolean} hidden
398
+ */
399
+ setHidden(hidden) {
400
+ this.selectMenu.setHidden(hidden);
401
+ }
402
+
403
+ get isOpen() {
404
+ return this.selectMenu.isOpen;
405
+ }
406
+
407
+ /**
408
+ * @description Dispatch a resolved item. For custom items, calls `raw.action($, ctx)`. For plugin
409
+ * items, routes to `plugin.open/show/action` or `commandDispatcher.run` (built-ins). For dropdown-free
410
+ * items, toggles the hover flyout
411
+ * @param {ResolvedItem | SubmenuChild | null} item
412
+ * @param {*} [ctx] - Context passed to custom-item `action` (host-specific shape).
413
+ * @returns {boolean} `true` if a plugin was dispatched
414
+ */
415
+ dispatch(item, ctx) {
416
+ if (!item) return false;
417
+ const asChild = /** @type {SubmenuChild} */ (/** @type {unknown} */ (item));
418
+ if (asChild.pluginName && asChild.element) {
419
+ const plugin = this.#$.plugins[asChild.pluginName];
420
+ plugin?.action?.(asChild.element);
421
+ this.#$.history.push(false);
422
+ return true;
423
+ }
424
+ const resolved = /** @type {ResolvedItem} */ (item);
425
+ if (resolved.kind === 'custom') {
426
+ try {
427
+ resolved.raw.action(this.#$, ctx);
428
+ } catch (e) {
429
+ console.error('[SUNEDITOR.commandMenu.customAction]', e);
430
+ }
431
+ return false;
432
+ }
433
+ if (resolved.kind === 'dropdownFree') {
434
+ const plugin = this.#$.plugins[resolved.name];
435
+ if (!plugin) return false;
436
+ if (this.#flyoutState?.plugin === plugin) {
437
+ this.#closeFlyout();
438
+ return false;
439
+ }
440
+ const idx = this.selectMenu.items.indexOf(/** @type {*} */ (resolved));
441
+ const anchorLi = idx >= 0 ? this.selectMenu.menus[idx] : null;
442
+ if (anchorLi) this.#openFlyout(resolved.name, plugin, anchorLi);
443
+ return false;
444
+ }
445
+ const plugin = resolved.name ? this.#$.plugins[resolved.name] : null;
446
+ if (plugin) {
447
+ const type = resolved.type || '';
448
+ if (/modal/.test(type)) plugin.open?.();
449
+ else if (/browser/.test(type)) plugin.open?.(null);
450
+ else if (/popup/.test(type)) plugin.show?.();
451
+ else plugin.action?.(dom.utils.createElement('BUTTON', { 'data-command': resolved.name }));
452
+ this.#$.history.push(false);
453
+ return true;
454
+ }
455
+ if (resolved.command) {
456
+ this.#$.commandDispatcher.run(resolved.command, null, null);
457
+ }
458
+ return false;
459
+ }
460
+
461
+ /**
462
+ * @description Mousemove handler on the menu form. For dropdown-free rows.
463
+ * @param {MouseEvent} e
464
+ */
465
+ #onMenuMouseMove(e) {
466
+ if (this.#freeMap.size === 0) return;
467
+ const target = /** @type {HTMLElement} */ (e.target);
468
+ if (this.#flyoutState?.dropdown?.contains(target)) return;
469
+
470
+ const li = target.closest?.('li[data-index]');
471
+ if (!li) return;
472
+
473
+ const idx = Number(li.getAttribute('data-index'));
474
+ const free = this.#freeMap.get(idx);
475
+ if (free) {
476
+ if (this.#flyoutState?.plugin !== free.plugin) {
477
+ this.#openFlyout(free.name, free.plugin, free.li);
478
+ }
479
+ } else if (this.#flyoutState) {
480
+ this.#closeFlyout();
481
+ }
482
+ }
483
+
484
+ /**
485
+ * @description Open a dropdown-free plugin's flyout.
486
+ * @param {string} name
487
+ * @param {*} plugin
488
+ * @param {HTMLElement} anchorLi
489
+ */
490
+ #openFlyout(name, plugin, anchorLi) {
491
+ if (this.#flyoutState) this.#closeFlyout();
492
+ const dropdown = this.#$.menu?.targetMap?.[name];
493
+ if (!dropdown || !this.selectMenu.form) return;
494
+
495
+ const originalParent = dropdown.parentNode;
496
+ this.selectMenu.form.appendChild(dropdown);
497
+ this.#positionFlyout(dropdown, anchorLi, this.selectMenu.form);
498
+
499
+ dom.utils.addClass(anchorLi, 'se-submenu-open');
500
+ plugin.on?.(anchorLi);
501
+
502
+ // Clicking inside the flyout's dropdown should dismiss both the flyout and the parent menu —
503
+ // matches the toolbar behavior where a click commits the selection.
504
+ const evClick = this.#$.eventManager.addEvent(dropdown, 'click', () =>
505
+ _w.setTimeout(() => {
506
+ this.#closeFlyout();
507
+ this.selectMenu.close();
508
+ }, 0),
509
+ );
510
+
511
+ this.#flyoutState = { dropdown, plugin, originalParent, anchorLi, evClick };
512
+ }
513
+
514
+ /**
515
+ * @description Close the open flyout (if any). Restores the dropdown DOM to its original parent
516
+ * (the menuTray) so the toolbar can use it again.
517
+ */
518
+ #closeFlyout() {
519
+ const s = this.#flyoutState;
520
+ if (!s) return;
521
+ this.#flyoutState = null;
522
+
523
+ this.#$?.eventManager.removeEvent(s.evClick);
524
+ s.dropdown.style.cssText = '';
525
+ s.dropdown.style.display = 'none';
526
+
527
+ if (s.anchorLi) dom.utils.removeClass(s.anchorLi, 'se-submenu-open');
528
+
529
+ if (s.originalParent && s.originalParent !== s.dropdown.parentNode) {
530
+ s.originalParent.appendChild(s.dropdown);
531
+ }
532
+
533
+ s.plugin?.off?.();
534
+ }
535
+
536
+ /**
537
+ * @description Place `el` next to `anchor`, coordinates relative to `container`. Mirrors
538
+ * `SelectMenu.#openSubmenu`: prefers the text-direction side, flips on overflow, shifts on
539
+ * double overflow.
540
+ * @param {HTMLElement} el
541
+ * @param {HTMLElement} anchor
542
+ * @param {HTMLElement} container
543
+ */
544
+ #positionFlyout(el, anchor, container) {
545
+ const a = anchor.getBoundingClientRect();
546
+ const c = container.getBoundingClientRect();
547
+ const isRtl = !!this.#$.options.get('_rtl');
548
+
549
+ el.style.position = 'absolute';
550
+ el.style.right = '';
551
+ el.style.visibility = 'hidden';
552
+ el.style.display = 'block';
553
+
554
+ const elW = el.offsetWidth;
555
+ const elH = el.offsetHeight;
556
+ const vpW = _w.innerWidth;
557
+ const vpH = _w.innerHeight;
558
+ const gap = 4;
559
+
560
+ // Horizontal: preferred side follows text direction. Flip if it overflows; if both overflow,
561
+ // keep the smaller-overflow side and shift inward by the missing amount.
562
+ const rightVP = a.right + gap;
563
+ const leftVP = a.left - elW - gap;
564
+ const rightOverflow = Math.max(0, rightVP + elW - vpW);
565
+ const leftOverflow = Math.max(0, -leftVP);
566
+ const prefer = isRtl ? 'left' : 'right';
567
+ let leftPx;
568
+ if (prefer === 'right') {
569
+ if (rightOverflow === 0) leftPx = rightVP;
570
+ else if (leftOverflow === 0) leftPx = leftVP;
571
+ else if (rightOverflow <= leftOverflow) leftPx = rightVP - rightOverflow;
572
+ else leftPx = leftVP + leftOverflow;
573
+ } else {
574
+ if (leftOverflow === 0) leftPx = leftVP;
575
+ else if (rightOverflow === 0) leftPx = rightVP;
576
+ else if (leftOverflow <= rightOverflow) leftPx = leftVP + leftOverflow;
577
+ else leftPx = rightVP - rightOverflow;
578
+ }
579
+ el.style.left = leftPx - c.left + 'px';
580
+
581
+ // Vertical: top-aligned with anchor, flip on overflow.
582
+ const topVP = a.top;
583
+ const bottomVP = a.bottom - elH;
584
+ const downOverflow = Math.max(0, topVP + elH - vpH);
585
+ const upOverflow = Math.max(0, -bottomVP);
586
+ let topPx;
587
+ if (downOverflow === 0) topPx = topVP;
588
+ else if (upOverflow === 0) topPx = bottomVP;
589
+ else if (downOverflow <= upOverflow) topPx = topVP - downOverflow;
590
+ else topPx = bottomVP + upOverflow;
591
+ el.style.top = topPx - c.top + 'px';
592
+
593
+ el.style.visibility = '';
594
+ }
595
+ }
596
+
597
+ export default CommandMenu;