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,297 @@
1
+ import { PluginField } from '../../interfaces';
2
+ import { Controller } from '../../modules/contract';
3
+ import { CommandMenu } from '../../modules/ui';
4
+ import { dom, converter, keyCodeMap } from '../../helper';
5
+ import { ResolveButton } from '../../core/section/constructor';
6
+
7
+ const { debounce } = converter;
8
+
9
+ /**
10
+ * @typedef {Object} SlashCommandItem
11
+ * @property {string} key - Unique identifier for the item.
12
+ * @property {string} title - Display label.
13
+ * @property {string} [icon] - Icon key from `$.icons` (e.g. `'h1'`) or a raw HTML string (e.g. `'<svg>...</svg>'`).
14
+ * @property {string[]} [keywords] - Extra search terms used in addition to the title.
15
+ * @property {function(SunEditor.Deps, SlashCommandActionContext): void | Promise<void>} action - Invoked when the item is selected.
16
+ * The trigger string (e.g. `/hea`) is already removed from the editor and the caret restored to that position before `action` runs.
17
+ * The action owns its history entry — calling an L3 wrapper (`$.html.*`, `$.format.*`, `$.inline.apply`) auto-pushes; otherwise call `$.history.push(false)` manually.
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} SlashCommandActionContext
22
+ * @property {string} triggerChar - The configured trigger character (e.g. `'/'`).
23
+ * @property {string} query - The text the user typed after the trigger (e.g. `'hea'`).
24
+ * @property {SlashCommandItem} item - The selected item.
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} SlashCommandPluginOptions
29
+ * @property {string} [triggerChar='/'] - Character that opens the command menu. Single character recommended.
30
+ * @property {Array<SlashCommandItem | string>} items - Menu entries. Strings resolve via `ResolveButton`
31
+ * (plugin names, built-in commands like `'bold'`); objects are custom items with their own `action`.
32
+ * Required.
33
+ * @property {number} [delayTime=120] - Debounce delay (ms) before the input is inspected for the trigger.
34
+ * @property {number} [limitSize=10] - Maximum number of items shown in the dropdown.
35
+ * @property {string} [emptyMessage] - Message shown when no items match the query. If unset, the menu closes on no match.
36
+ * @property {function(SlashCommandItem, { icons: Object }): string} [renderItem] - Custom item HTML renderer.
37
+ * Applied only to custom item objects; plugin-name entries always render with the canonical BlockHandle row.
38
+ * ```js
39
+ * SUNEDITOR.create('#editor', {
40
+ * plugins: [slashCommand],
41
+ * slashCommand: {
42
+ * triggerChar: '/',
43
+ * items: [
44
+ * {
45
+ * key: 'h1',
46
+ * title: 'Heading 1',
47
+ * icon: 'h1',
48
+ * keywords: ['header', 'title'],
49
+ * action: ($) => $.format.applyBlock(document.createElement('H1')),
50
+ * },
51
+ * 'bold',
52
+ * 'image',
53
+ * 'blockStyle',
54
+ * ],
55
+ * },
56
+ * });
57
+ * ```
58
+ */
59
+
60
+ /**
61
+ * @class
62
+ * @description Slash Command plugin
63
+ * - Notion / Tiptap style "/" command menu. Triggered by the configured character (default `'/'`).
64
+ * - Menu rendering + dispatch is delegated to {@link CommandMenu}, which is also used by BlockHandle's
65
+ * action menu — both menus share the exact same row HTML and dispatch behavior.
66
+ * - SlashCommand-specific concerns kept here: input watching, debounce, trigger detection,
67
+ * trigger text deletion before dispatch, empty-state row.
68
+ */
69
+ class SlashCommand extends PluginField {
70
+ static key = 'slashCommand';
71
+ static className = '';
72
+
73
+ #triggerChar;
74
+ #limitSize;
75
+ #emptyMessage;
76
+
77
+ /** @type {CommandMenu} */
78
+ #menu;
79
+
80
+ #lastTriggerPos = 0;
81
+ #anchorOffset = 0;
82
+ #anchorNode = null;
83
+ #internalClose = false;
84
+
85
+ /**
86
+ * @constructor
87
+ * @param {SunEditor.Kernel} kernel
88
+ * @param {SlashCommandPluginOptions} pluginOptions
89
+ */
90
+ constructor(kernel, pluginOptions) {
91
+ super(kernel);
92
+
93
+ this.#triggerChar =
94
+ typeof pluginOptions.triggerChar === 'string' && pluginOptions.triggerChar.length > 0
95
+ ? pluginOptions.triggerChar
96
+ : '/';
97
+ this.#limitSize =
98
+ typeof pluginOptions.limitSize === 'number' && pluginOptions.limitSize > 0 ? pluginOptions.limitSize : 10;
99
+ this.#emptyMessage = typeof pluginOptions.emptyMessage === 'string' ? pluginOptions.emptyMessage : '';
100
+ const delayTime = typeof pluginOptions.delayTime === 'number' ? pluginOptions.delayTime : 120;
101
+
102
+ this.#menu = new CommandMenu(this, this.$, {
103
+ items: Array.isArray(pluginOptions.items) ? pluginOptions.items : [],
104
+ resolveButton: ResolveButton,
105
+ renderCustomItem: typeof pluginOptions.renderItem === 'function' ? pluginOptions.renderItem : null,
106
+ selectMenuParams: {
107
+ position: 'bottom-left',
108
+ dir: 'ltr',
109
+ minWidth: '200px',
110
+ maxHeight: '320px',
111
+ closeMethod: () => this.#onMenuClose(),
112
+ },
113
+ });
114
+
115
+ const controllerEl = CreateHTML_controller();
116
+ this.controller = new Controller(
117
+ this,
118
+ this.$,
119
+ controllerEl,
120
+ {
121
+ position: 'bottom',
122
+ escGuard: () => this.#menu.hasOpenSubPanel(),
123
+ },
124
+ null,
125
+ );
126
+
127
+ this.#menu.attach(/** @type {HTMLElement} */ (controllerEl.firstElementChild), this.#onSelectItem.bind(this), {
128
+ class: 'se-block-action-menu se-slash-command-menu',
129
+ });
130
+
131
+ this.onInput = debounce(this.onInput.bind(this), delayTime);
132
+ }
133
+
134
+ /**
135
+ * @hook Editor.EventManager
136
+ * @description ESC while the menu is still pending must not let that scheduled `onInput` re-open the menu after the dismiss.
137
+ * @type {SunEditor.Hook.Event.OnKeyDown}
138
+ * @param {SunEditor.HookParams.KeyEvent} params
139
+ */
140
+ onKeyDown({ event }) {
141
+ if (keyCodeMap.isEsc(event.code)) /** @type {{ cancel?: () => void }} */ (this.onInput).cancel?.();
142
+ }
143
+
144
+ /**
145
+ * @hook Editor.EventManager
146
+ * @type {SunEditor.Hook.Event.OnInput}
147
+ */
148
+ onInput() {
149
+ const items = this.#menu.getItems();
150
+ if (items.length === 0) return;
151
+
152
+ const sel = this.$.selection.get();
153
+ if (!sel.rangeCount) {
154
+ this.#closeMenu();
155
+ return;
156
+ }
157
+
158
+ const anchorNode = sel.anchorNode;
159
+ const anchorOffset = sel.anchorOffset;
160
+ if (!anchorNode || typeof anchorNode.textContent !== 'string') {
161
+ this.#closeMenu();
162
+ return;
163
+ }
164
+
165
+ const textBeforeCursor = anchorNode.textContent.substring(0, anchorOffset);
166
+ const trigger = this.#triggerChar;
167
+ const lastPos = textBeforeCursor.lastIndexOf(trigger);
168
+ if (lastPos === -1) {
169
+ this.#closeMenu();
170
+ return;
171
+ }
172
+
173
+ const query = textBeforeCursor.substring(lastPos + trigger.length, anchorOffset);
174
+ // Reject when the query contains whitespace (user moved past the slash word).
175
+ if (/\s/.test(query)) {
176
+ this.#closeMenu();
177
+ return;
178
+ }
179
+
180
+ // Trigger must sit at the start of the line or be preceded by whitespace / zero-width.
181
+ const beforeChar = textBeforeCursor[lastPos - 1];
182
+ if (beforeChar && beforeChar.trim() !== '' && !dom.check.isZeroWidth(beforeChar)) {
183
+ this.#closeMenu();
184
+ return;
185
+ }
186
+
187
+ const filtered = this.#menu.filter(query, this.#limitSize);
188
+
189
+ if (filtered.length === 0) {
190
+ if (this.#emptyMessage) {
191
+ this.#renderEmpty(anchorNode);
192
+ this.#cacheAnchor(anchorNode, lastPos, anchorOffset);
193
+ } else {
194
+ this.#closeMenu();
195
+ }
196
+ return;
197
+ }
198
+
199
+ this.controller.open(anchorNode, null, { isWWTarget: true, initMethod: null, addOffset: null });
200
+ this.#menu.createRows(filtered);
201
+ this.#menu.open();
202
+ this.#menu.setItem(0);
203
+ this.#cacheAnchor(anchorNode, lastPos, anchorOffset);
204
+ }
205
+
206
+ /**
207
+ * @description Close the menu from the plugin itself (invalid query, or after a selection). Flags
208
+ * the close as internal so `#onMenuClose` does not treat it as a user dismiss.
209
+ */
210
+ #closeMenu() {
211
+ this.#internalClose = true;
212
+ this.#menu.close();
213
+ this.#internalClose = false;
214
+ }
215
+
216
+ /**
217
+ * @description SelectMenu `closeMethod`. On a user dismiss (ESC / outside-click) — i.e. not an
218
+ * internal close and not a selection — drop the typed "/query" and restore the caret to where the
219
+ * trigger was, leaving the editor in a clean state. Always tears down the controller.
220
+ */
221
+ #onMenuClose() {
222
+ if (!this.#internalClose) this.#removeTrigger();
223
+ this.#anchorNode = null;
224
+ this.controller.close();
225
+ }
226
+
227
+ /**
228
+ * @description Remove the trigger + query text (`/hea`) and collapse the caret to the trigger
229
+ * position. No-op if the cached anchor is stale (text changed / node detached).
230
+ */
231
+ #removeTrigger() {
232
+ const anchorNode = this.#anchorNode;
233
+ if (!anchorNode || !anchorNode.parentNode) return;
234
+ if (anchorNode.textContent?.[this.#lastTriggerPos] !== this.#triggerChar) return;
235
+
236
+ this.$.selection.setRange(anchorNode, this.#lastTriggerPos, anchorNode, this.#anchorOffset);
237
+ const range = this.$.selection.getRange();
238
+ if (range && !range.collapsed) this.$.html.remove();
239
+ }
240
+
241
+ /**
242
+ * @param {Node} anchorNode
243
+ * @param {number} lastPos
244
+ * @param {number} anchorOffset
245
+ */
246
+ #cacheAnchor(anchorNode, lastPos, anchorOffset) {
247
+ this.#anchorNode = anchorNode;
248
+ this.#lastTriggerPos = lastPos;
249
+ this.#anchorOffset = anchorOffset;
250
+ }
251
+
252
+ /**
253
+ * @description Render the no-match row as a single non-clickable entry.
254
+ * @param {Node} targetNode
255
+ */
256
+ #renderEmpty(targetNode) {
257
+ this.controller.open(targetNode, null, { isWWTarget: true, initMethod: null, addOffset: null });
258
+ this.#menu.selectMenu.create([null], [`<span class="se-slash-empty">${this.#emptyMessage}</span>`]);
259
+ this.#menu.open();
260
+ }
261
+
262
+ /**
263
+ * @description Click handler. Removes the trigger string from the editor, then hands the item
264
+ * to {@link CommandMenu#dispatch} — which routes to `plugin.open/show/action`,
265
+ * `commandDispatcher.run`, or the custom item's `action` callback.
266
+ * @param {import('../../modules/ui/CommandMenu.js').ResolvedItem | null} item
267
+ * @returns {boolean | undefined}
268
+ */
269
+ #onSelectItem(item) {
270
+ if (!item) return false;
271
+
272
+ const anchorNode = this.#anchorNode;
273
+ if (!anchorNode) return false;
274
+
275
+ const triggerChar = this.#triggerChar;
276
+ const query = anchorNode.textContent.substring(this.#lastTriggerPos + triggerChar.length, this.#anchorOffset);
277
+
278
+ // Remove the trigger + query, leaving the caret at the trigger position so the action
279
+ // (insert block, run command, etc.) operates from a clean cursor.
280
+ this.$.selection.setRange(anchorNode, this.#lastTriggerPos, anchorNode, this.#anchorOffset);
281
+ const range = this.$.selection.getRange();
282
+ if (range && !range.collapsed) this.$.html.remove();
283
+
284
+ this.#closeMenu();
285
+
286
+ this.#menu.dispatch(item, { triggerChar, query, item: item.raw });
287
+ }
288
+ }
289
+
290
+ /**
291
+ * @returns {HTMLElement}
292
+ */
293
+ function CreateHTML_controller() {
294
+ return dom.utils.createElement('DIV', { class: 'se-controller se-empty-controller' }, '<div></div>');
295
+ }
296
+
297
+ export default SlashCommand;
@@ -8,6 +8,7 @@ import list_numbered from './command/list_numbered';
8
8
 
9
9
  // field
10
10
  import autocomplete from './field/autocomplete';
11
+ import slashCommand from './field/slashCommand';
11
12
 
12
13
  // dropdown
13
14
  import align from './dropdown/align';
@@ -55,6 +56,7 @@ export {
55
56
  list_bulleted,
56
57
  list_numbered,
57
58
  autocomplete,
59
+ slashCommand,
58
60
  align,
59
61
  font,
60
62
  fontColor,
@@ -92,6 +94,7 @@ export default {
92
94
  list_bulleted,
93
95
  list_numbered,
94
96
  autocomplete,
97
+ slashCommand,
95
98
  align,
96
99
  font,
97
100
  fontColor,
@@ -131,7 +131,9 @@ class FontSize extends PluginInput {
131
131
 
132
132
  // create HTML
133
133
  this.unitMap = { ...DEFAULT_UNIT_MAP, ...(pluginOptions.unitMap || {}) };
134
- this.sizeUnit = /text/.test(pluginOptions.sizeUnit) ? '' : pluginOptions.sizeUnit || this.$.options.get('fontSizeUnits')[0];
134
+ this.sizeUnit = /text/.test(pluginOptions.sizeUnit)
135
+ ? ''
136
+ : pluginOptions.sizeUnit || this.$.options.get('fontSizeUnits')[0];
135
137
 
136
138
  const unitMap = this.unitMap[this.sizeUnit || 'text'];
137
139
  const menu = CreateHTML(this.$, unitMap, this.sizeUnit, pluginOptions.showDefaultSizeLabel);
@@ -152,25 +154,47 @@ class FontSize extends PluginInput {
152
154
  if (showIncDec) {
153
155
  this.beforeItem = dom.utils.createElement(
154
156
  'button',
155
- { class: 'se-btn se-tooltip se-sub-btn', type: 'button', 'data-command': FontSize.key, 'data-type': 'command', 'data-value': 'dec' },
157
+ {
158
+ class: 'se-btn se-tooltip se-sub-btn',
159
+ type: 'button',
160
+ 'data-command': FontSize.key,
161
+ 'data-type': 'command',
162
+ 'data-value': 'dec',
163
+ },
156
164
  `${this.$.icons.minus}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.decrease}</span></span>`,
157
165
  );
158
166
  this.afterItem = dom.utils.createElement(
159
167
  'button',
160
- { class: 'se-btn se-tooltip se-sub-btn', type: 'button', 'data-command': FontSize.key, 'data-type': 'command', 'data-value': 'inc' },
168
+ {
169
+ class: 'se-btn se-tooltip se-sub-btn',
170
+ type: 'button',
171
+ 'data-command': FontSize.key,
172
+ 'data-type': 'command',
173
+ 'data-value': 'inc',
174
+ },
161
175
  `${this.$.icons.plus}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.increase}</span></span>`,
162
176
  );
163
177
  } else if (!disableInput) {
164
178
  this.afterItem = dom.utils.createElement(
165
179
  'button',
166
- { class: 'se-btn se-tooltip se-sub-arrow-btn', type: 'button', 'data-command': FontSize.key, 'data-type': 'dropdown' },
180
+ {
181
+ class: 'se-btn se-tooltip se-sub-arrow-btn',
182
+ type: 'button',
183
+ 'data-command': FontSize.key,
184
+ 'data-type': 'dropdown',
185
+ },
167
186
  `${this.$.icons.arrow_down}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.fontSize}</span></span>`,
168
187
  );
169
188
  this.$.menu.initDropdownTarget({ key: FontSize.key, type: 'dropdown' }, menu);
170
189
  } else if (disableInput && !showIncDec) {
171
190
  this.replaceButton = dom.utils.createElement(
172
191
  'button',
173
- { class: 'se-btn se-tooltip se-btn-select se-btn-tool-font-size', type: 'button', 'data-command': FontSize.key, 'data-type': 'dropdown' },
192
+ {
193
+ class: 'se-btn se-tooltip se-btn-select se-btn-tool-font-size',
194
+ type: 'button',
195
+ 'data-command': FontSize.key,
196
+ 'data-type': 'dropdown',
197
+ },
174
198
  `<span class="se-txt __se__font_size">${this.$.lang.fontSize}</span>${this.$.icons.arrow_down}<span class="se-tooltip-inner"><span class="se-tooltip-text">${this.$.lang.fontSize}</span></span>`,
175
199
  );
176
200
  this.$.menu.initDropdownTarget({ key: FontSize.key, type: 'dropdown' }, menu);
@@ -268,7 +292,9 @@ class FontSize extends PluginInput {
268
292
  const { max, min } = this.unitMap[unit];
269
293
  value = value > max ? max : value < min ? min : value;
270
294
 
271
- const newNode = dom.utils.createElement('SPAN', { style: 'font-size: ' + this.#setSize(target, value + unit) + ';' });
295
+ const newNode = dom.utils.createElement('SPAN', {
296
+ style: 'font-size: ' + this.#setSize(target, value + unit) + ';',
297
+ });
272
298
  this.$.inline.apply(newNode, { stylesToModify: ['font-size'], nodesToRemove: null, strictRemove: null });
273
299
  } finally {
274
300
  this.isInputActive = false;
@@ -347,7 +373,8 @@ class FontSize extends PluginInput {
347
373
  value: this.sizeUnit ? 0 : '',
348
374
  };
349
375
 
350
- const size = typeof target === 'string' ? target : dom.check.isInputElement(target) ? target.value : target.textContent;
376
+ const size =
377
+ typeof target === 'string' ? target : dom.check.isInputElement(target) ? target.value : target.textContent;
351
378
  const splitValue = this.sizeUnit ? size.split(/(\d+)/) : [size, ''];
352
379
 
353
380
  let unit = (splitValue.pop() || '').trim().toLowerCase();
@@ -375,7 +402,9 @@ class FontSize extends PluginInput {
375
402
  if (dom.check.isInputElement(target)) {
376
403
  return (target.value = String(value));
377
404
  } else {
378
- return (target.textContent = String(this.sizeUnit ? value : this.unitMap.text.list.find((v) => v.size === value)?.title || value));
405
+ return (target.textContent = String(
406
+ this.sizeUnit ? value : this.unitMap.text.list.find((v) => v.size === value)?.title || value,
407
+ ));
379
408
  }
380
409
  }
381
410
  }
@@ -67,7 +67,13 @@ class PageNavigator extends PluginInput {
67
67
  * @returns {HTMLInputElement}
68
68
  */
69
69
  function CreateInner() {
70
- return /** @type {HTMLInputElement} */ (dom.utils.createElement('input', { type: 'number', class: 'se-not-arrow-text', placeholder: '1', value: '1', min: '1' }, null));
70
+ return /** @type {HTMLInputElement} */ (
71
+ dom.utils.createElement(
72
+ 'input',
73
+ { type: 'number', class: 'se-not-arrow-text', placeholder: '1', value: '1', min: '1' },
74
+ null,
75
+ )
76
+ );
71
77
  }
72
78
 
73
79
  export default PageNavigator;
@@ -76,16 +76,30 @@ class Audio_ extends PluginModal {
76
76
 
77
77
  // define plugin options
78
78
  this.pluginOptions = {
79
- defaultWidth: !pluginOptions.defaultWidth ? '' : numbers.is(pluginOptions.defaultWidth) ? pluginOptions.defaultWidth + 'px' : pluginOptions.defaultWidth,
80
- defaultHeight: !pluginOptions.defaultHeight ? '' : numbers.is(pluginOptions.defaultHeight) ? pluginOptions.defaultHeight + 'px' : pluginOptions.defaultHeight,
79
+ defaultWidth: !pluginOptions.defaultWidth
80
+ ? ''
81
+ : numbers.is(pluginOptions.defaultWidth)
82
+ ? pluginOptions.defaultWidth + 'px'
83
+ : pluginOptions.defaultWidth,
84
+ defaultHeight: !pluginOptions.defaultHeight
85
+ ? ''
86
+ : numbers.is(pluginOptions.defaultHeight)
87
+ ? pluginOptions.defaultHeight + 'px'
88
+ : pluginOptions.defaultHeight,
81
89
  createFileInput: !!pluginOptions.createFileInput,
82
- createUrlInput: pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput ? true : pluginOptions.createUrlInput,
90
+ createUrlInput:
91
+ pluginOptions.createUrlInput === undefined || !pluginOptions.createFileInput
92
+ ? true
93
+ : pluginOptions.createUrlInput,
83
94
  uploadUrl: typeof pluginOptions.uploadUrl === 'string' ? pluginOptions.uploadUrl : null,
84
95
  uploadHeaders: pluginOptions.uploadHeaders || null,
85
96
  uploadSizeLimit: numbers.get(pluginOptions.uploadSizeLimit, 0),
86
97
  uploadSingleSizeLimit: numbers.get(pluginOptions.uploadSingleSizeLimit, 0),
87
98
  allowMultiple: !!pluginOptions.allowMultiple,
88
- acceptedFormats: typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*' ? 'audio/*' : pluginOptions.acceptedFormats.trim() || 'audio/*',
99
+ acceptedFormats:
100
+ typeof pluginOptions.acceptedFormats !== 'string' || pluginOptions.acceptedFormats.trim() === '*'
101
+ ? 'audio/*'
102
+ : pluginOptions.acceptedFormats.trim() || 'audio/*',
89
103
  audioTagAttributes: pluginOptions.audioTagAttributes || null,
90
104
  insertBehavior: pluginOptions.insertBehavior,
91
105
  };
@@ -124,7 +138,11 @@ class Audio_ extends PluginModal {
124
138
 
125
139
  // init
126
140
  if (this.audioInputFile) {
127
- this.$.eventManager.addEvent(modalEl.querySelector('.se-modal-files-edge-button'), 'click', this.#RemoveSelectedFiles.bind(this, this.audioUrlFile, this.preview));
141
+ this.$.eventManager.addEvent(
142
+ modalEl.querySelector('.se-modal-files-edge-button'),
143
+ 'click',
144
+ this.#RemoveSelectedFiles.bind(this, this.audioUrlFile, this.preview),
145
+ );
128
146
  if (this.audioUrlFile) {
129
147
  this.$.eventManager.addEvent(this.audioInputFile, 'change', this.#FileInputChange.bind(this));
130
148
  }
@@ -177,12 +195,15 @@ class Audio_ extends PluginModal {
177
195
  */
178
196
  modalOn(isUpdate) {
179
197
  if (!isUpdate) {
180
- if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.setAttribute('multiple', 'multiple');
198
+ if (this.audioInputFile && this.pluginOptions.allowMultiple)
199
+ this.audioInputFile.setAttribute('multiple', 'multiple');
181
200
  } else if (this.#element) {
182
201
  this.#urlValue = this.preview.textContent = this.audioUrlFile.value = this.#element.src;
183
- if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.removeAttribute('multiple');
202
+ if (this.audioInputFile && this.pluginOptions.allowMultiple)
203
+ this.audioInputFile.removeAttribute('multiple');
184
204
  } else {
185
- if (this.audioInputFile && this.pluginOptions.allowMultiple) this.audioInputFile.removeAttribute('multiple');
205
+ if (this.audioInputFile && this.pluginOptions.allowMultiple)
206
+ this.audioInputFile.removeAttribute('multiple');
186
207
  }
187
208
  }
188
209
 
@@ -220,7 +241,8 @@ class Audio_ extends PluginModal {
220
241
  controllerAction(target) {
221
242
  switch (target.getAttribute('data-command')) {
222
243
  case 'update':
223
- if (this.audioUrlFile) this.#urlValue = this.preview.textContent = this.audioUrlFile.value = this.#element.src;
244
+ if (this.audioUrlFile)
245
+ this.#urlValue = this.preview.textContent = this.audioUrlFile.value = this.#element.src;
224
246
  this.open();
225
247
  break;
226
248
  case 'copy': {
@@ -239,7 +261,13 @@ class Audio_ extends PluginModal {
239
261
  * @type {SunEditor.Hook.Component.Select}
240
262
  */
241
263
  componentSelect(target) {
242
- this.figure.open(target, { nonResizing: true, nonSizeInfo: true, nonBorder: true, figureTarget: true, infoOnly: false });
264
+ this.figure.open(target, {
265
+ nonResizing: true,
266
+ nonSizeInfo: true,
267
+ nonBorder: true,
268
+ figureTarget: true,
269
+ infoOnly: false,
270
+ });
243
271
  this.#ready(target);
244
272
  }
245
273
 
@@ -253,7 +281,11 @@ class Audio_ extends PluginModal {
253
281
  const container = figure.container || element;
254
282
  const focusEl = container.previousElementSibling || container.nextElementSibling;
255
283
 
256
- const message = await this.$.eventManager.triggerEvent('onAudioDeleteBefore', { element: element, container: figure, url: element.getAttribute('src') });
284
+ const message = await this.$.eventManager.triggerEvent('onAudioDeleteBefore', {
285
+ element: element,
286
+ container: figure,
287
+ url: element.getAttribute('src'),
288
+ });
257
289
  if (message === false) return;
258
290
 
259
291
  const emptyDiv = container.parentNode;
@@ -293,7 +325,8 @@ class Audio_ extends PluginModal {
293
325
 
294
326
  s = f.size;
295
327
  if (singleSizeLimit > 0 && s > singleSizeLimit) {
296
- const err = '[SUNEDITOR.audioUpload.fail] Size of uploadable single file: ' + singleSizeLimit / 1000 + 'KB';
328
+ const err =
329
+ '[SUNEDITOR.audioUpload.fail] Size of uploadable single file: ' + singleSizeLimit / 1000 + 'KB';
297
330
  const message = await this.$.eventManager.triggerEvent('onAudioUploadError', {
298
331
  error: err,
299
332
  limitSize: singleSizeLimit,
@@ -313,7 +346,12 @@ class Audio_ extends PluginModal {
313
346
  const limitSize = this.pluginOptions.uploadSizeLimit;
314
347
  if (limitSize > 0 && fileSize + this.fileManager.getSize() > limitSize) {
315
348
  const err = '[SUNEDITOR.audioUpload.fail] Size of uploadable total audios: ' + limitSize / 1000 + 'KB';
316
- const message = await this.$.eventManager.triggerEvent('onAudioUploadError', { error: err, limitSize, currentSize: this.fileManager.getSize(), uploadSize: fileSize });
349
+ const message = await this.$.eventManager.triggerEvent('onAudioUploadError', {
350
+ error: err,
351
+ limitSize,
352
+ currentSize: this.fileManager.getSize(),
353
+ uploadSize: fileSize,
354
+ });
317
355
 
318
356
  this.$.ui.alertOpen(message === NO_EVENT ? err : message || err, 'error');
319
357
 
@@ -396,7 +434,12 @@ class Audio_ extends PluginModal {
396
434
  this.fileManager.setFileData(element, file);
397
435
  element.src = src;
398
436
  const figure = Figure.CreateContainer(element, 'se-flex-component');
399
- if (!this.$.component.insert(figure.container, { scrollTo: isLast ? true : false, insertBehavior: isLast ? this.pluginOptions.insertBehavior : 'line' })) {
437
+ if (
438
+ !this.$.component.insert(figure.container, {
439
+ scrollTo: isLast ? true : false,
440
+ insertBehavior: isLast ? this.pluginOptions.insertBehavior : 'line',
441
+ })
442
+ ) {
400
443
  if (isLast) this.$.focusManager.focus();
401
444
  return;
402
445
  }
@@ -458,7 +501,9 @@ class Audio_ extends PluginModal {
458
501
  const w = this.#defaultWidth;
459
502
  const h = this.#defaultHeight;
460
503
  /** @type {HTMLAudioElement} */
461
- const oAudio = dom.utils.createElement('AUDIO', { style: (w ? 'width:' + w + '; ' : '') + (h ? 'height:' + h + ';' : '') });
504
+ const oAudio = dom.utils.createElement('AUDIO', {
505
+ style: (w ? 'width:' + w + '; ' : '') + (h ? 'height:' + h + ';' : ''),
506
+ });
462
507
  this.#setTagAttrs(oAudio);
463
508
  return oAudio;
464
509
  }
@@ -489,7 +534,13 @@ class Audio_ extends PluginModal {
489
534
  if (!files) return;
490
535
 
491
536
  const uploadFiles = this.modal.isUpdate ? [files[0]] : files;
492
- this.fileManager.upload(this.pluginOptions.uploadUrl, this.pluginOptions.uploadHeaders, uploadFiles, this.#UploadCallBack.bind(this, info), this.#error.bind(this));
537
+ this.fileManager.upload(
538
+ this.pluginOptions.uploadUrl,
539
+ this.pluginOptions.uploadHeaders,
540
+ uploadFiles,
541
+ this.#UploadCallBack.bind(this, info),
542
+ this.#error.bind(this),
543
+ );
493
544
  }
494
545
 
495
546
  /**
@@ -555,7 +606,10 @@ class Audio_ extends PluginModal {
555
606
  * @param {HTMLInputElement} target - The target element.
556
607
  */
557
608
  #SetUrlInput(target) {
558
- this.#urlValue = this.preview.textContent = this.audioUrlFile.value = target.getAttribute('data-command') || target.src;
609
+ this.#urlValue =
610
+ this.preview.textContent =
611
+ this.audioUrlFile.value =
612
+ target.getAttribute('data-command') || target.src;
559
613
  this.audioUrlFile.focus();
560
614
  }
561
615