suneditor 3.1.4 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/dist/suneditor-contents.min.css +1 -1
  2. package/dist/suneditor.min.css +28 -1
  3. package/dist/suneditor.min.js +1 -1
  4. package/package.json +2 -1
  5. package/src/assets/design/color.css +12 -0
  6. package/src/assets/design/size.css +9 -2
  7. package/src/assets/icons/defaultIcons.js +96 -47
  8. package/src/assets/suneditor-contents.css +3 -2
  9. package/src/assets/suneditor.css +481 -31
  10. package/src/core/config/contextProvider.js +2 -1
  11. package/src/core/config/eventManager.js +41 -2
  12. package/src/core/config/optionProvider.js +23 -2
  13. package/src/core/editor.js +32 -14
  14. package/src/core/event/actions/index.js +76 -9
  15. package/src/core/event/effects/common.registry.js +2 -1
  16. package/src/core/event/effects/keydown.registry.js +169 -33
  17. package/src/core/event/effects/ruleHelpers.js +20 -6
  18. package/src/core/event/eventOrchestrator.js +162 -30
  19. package/src/core/event/handlers/handler_toolbar.js +4 -2
  20. package/src/core/event/handlers/handler_ww_dragDrop.js +10 -0
  21. package/src/core/event/handlers/handler_ww_input.js +84 -2
  22. package/src/core/event/handlers/handler_ww_key.js +82 -21
  23. package/src/core/event/handlers/handler_ww_mouse.js +45 -8
  24. package/src/core/event/ports.js +27 -13
  25. package/src/core/event/reducers/keydown.reducer.js +33 -5
  26. package/src/core/event/rules/keydown.rule.arrow.js +12 -2
  27. package/src/core/event/rules/keydown.rule.backspace.js +110 -15
  28. package/src/core/event/rules/keydown.rule.delete.js +62 -7
  29. package/src/core/event/rules/keydown.rule.enter.js +62 -31
  30. package/src/core/event/support/defaultLineManager.js +9 -2
  31. package/src/core/event/support/selectionState.js +13 -3
  32. package/src/core/kernel/coreKernel.js +1 -0
  33. package/src/core/logic/dom/char.js +13 -3
  34. package/src/core/logic/dom/format.js +171 -39
  35. package/src/core/logic/dom/html.js +350 -72
  36. package/src/core/logic/dom/inline.js +229 -34
  37. package/src/core/logic/dom/listFormat.js +111 -20
  38. package/src/core/logic/dom/nodeTransform.js +36 -11
  39. package/src/core/logic/dom/offset.js +116 -31
  40. package/src/core/logic/dom/selection.js +147 -29
  41. package/src/core/logic/panel/blockHandle.js +785 -0
  42. package/src/core/logic/panel/blockResolver.js +278 -0
  43. package/src/core/logic/panel/finder.js +16 -6
  44. package/src/core/logic/panel/menu.js +119 -18
  45. package/src/core/logic/panel/toolbar.js +75 -19
  46. package/src/core/logic/panel/viewer.js +76 -20
  47. package/src/core/logic/shell/_commandExecutor.js +44 -9
  48. package/src/core/logic/shell/commandDispatcher.js +46 -4
  49. package/src/core/logic/shell/component.js +102 -25
  50. package/src/core/logic/shell/focusManager.js +13 -3
  51. package/src/core/logic/shell/history.js +21 -6
  52. package/src/core/logic/shell/pluginManager.js +21 -13
  53. package/src/core/logic/shell/shortcuts.js +9 -1
  54. package/src/core/logic/shell/ui.js +139 -37
  55. package/src/core/schema/frameContext.js +34 -3
  56. package/src/core/schema/options.js +46 -4
  57. package/src/core/section/constructor.js +537 -106
  58. package/src/core/section/documentType.js +35 -6
  59. package/src/helper/converter.js +24 -7
  60. package/src/helper/dom/domCheck.js +25 -5
  61. package/src/helper/dom/domQuery.js +2 -1
  62. package/src/helper/dom/domUtils.js +17 -4
  63. package/src/helper/env.js +11 -2
  64. package/src/helper/keyCodeMap.js +6 -2
  65. package/src/helper/markdown.js +24 -5
  66. package/src/helper/msOffice.js +9 -2
  67. package/src/interfaces/plugins.js +1 -1
  68. package/src/langs/ckb.js +0 -1
  69. package/src/langs/cs.js +0 -1
  70. package/src/langs/da.js +0 -1
  71. package/src/langs/de.js +0 -1
  72. package/src/langs/en.js +0 -1
  73. package/src/langs/es.js +0 -1
  74. package/src/langs/fa.js +0 -1
  75. package/src/langs/fr.js +0 -1
  76. package/src/langs/he.js +0 -1
  77. package/src/langs/hu.js +0 -1
  78. package/src/langs/index.js +27 -1
  79. package/src/langs/it.js +0 -1
  80. package/src/langs/ja.js +0 -1
  81. package/src/langs/km.js +0 -1
  82. package/src/langs/ko.js +0 -1
  83. package/src/langs/lv.js +0 -1
  84. package/src/langs/nl.js +0 -1
  85. package/src/langs/pl.js +0 -1
  86. package/src/langs/pt_br.js +0 -1
  87. package/src/langs/ro.js +0 -1
  88. package/src/langs/ru.js +0 -1
  89. package/src/langs/se.js +0 -1
  90. package/src/langs/tr.js +0 -1
  91. package/src/langs/uk.js +0 -1
  92. package/src/langs/ur.js +0 -1
  93. package/src/langs/zh_cn.js +0 -1
  94. package/src/modules/contract/Browser.js +58 -14
  95. package/src/modules/contract/ColorPicker.js +12 -4
  96. package/src/modules/contract/Controller.js +37 -7
  97. package/src/modules/contract/Figure.js +156 -36
  98. package/src/modules/contract/HueSlider.js +27 -4
  99. package/src/modules/contract/Modal.js +50 -10
  100. package/src/modules/manager/FileManager.js +50 -8
  101. package/src/modules/ui/CommandMenu.js +597 -0
  102. package/src/modules/ui/ModalAnchorEditor.js +33 -6
  103. package/src/modules/ui/SelectMenu.js +481 -36
  104. package/src/modules/ui/index.js +1 -0
  105. package/src/plugins/browser/audioGallery.js +2 -1
  106. package/src/plugins/browser/fileBrowser.js +9 -2
  107. package/src/plugins/browser/fileGallery.js +2 -1
  108. package/src/plugins/browser/imageGallery.js +18 -3
  109. package/src/plugins/browser/videoGallery.js +13 -4
  110. package/src/plugins/command/blockquote.js +6 -1
  111. package/src/plugins/command/codeBlock.js +43 -5
  112. package/src/plugins/command/exportPDF.js +20 -4
  113. package/src/plugins/command/fileUpload.js +46 -11
  114. package/src/plugins/command/list_bulleted.js +6 -1
  115. package/src/plugins/command/list_numbered.js +6 -1
  116. package/src/plugins/dropdown/align.js +21 -33
  117. package/src/plugins/dropdown/backgroundColor.js +10 -2
  118. package/src/plugins/dropdown/blockStyle.js +17 -22
  119. package/src/plugins/dropdown/font.js +34 -34
  120. package/src/plugins/dropdown/hr.js +15 -43
  121. package/src/plugins/dropdown/layout.js +10 -27
  122. package/src/plugins/dropdown/lineHeight.js +10 -33
  123. package/src/plugins/dropdown/list.js +19 -25
  124. package/src/plugins/dropdown/paragraphStyle.js +16 -24
  125. package/src/plugins/dropdown/table/index.js +72 -17
  126. package/src/plugins/dropdown/table/render/table.html.js +3 -1
  127. package/src/plugins/dropdown/table/services/table.cell.js +33 -7
  128. package/src/plugins/dropdown/table/services/table.clipboard.js +5 -1
  129. package/src/plugins/dropdown/table/services/table.grid.js +40 -9
  130. package/src/plugins/dropdown/table/services/table.resize.js +55 -11
  131. package/src/plugins/dropdown/table/services/table.selection.js +27 -7
  132. package/src/plugins/dropdown/table/services/table.style.js +120 -20
  133. package/src/plugins/dropdown/template.js +13 -29
  134. package/src/plugins/dropdown/textStyle.js +22 -35
  135. package/src/plugins/field/autocomplete.js +8 -4
  136. package/src/plugins/field/slashCommand.js +297 -0
  137. package/src/plugins/index.js +3 -0
  138. package/src/plugins/input/fontSize.js +37 -8
  139. package/src/plugins/input/pageNavigator.js +7 -1
  140. package/src/plugins/modal/audio.js +71 -17
  141. package/src/plugins/modal/drawing.js +41 -11
  142. package/src/plugins/modal/embed.js +92 -22
  143. package/src/plugins/modal/image/index.js +114 -28
  144. package/src/plugins/modal/image/services/image.size.js +9 -2
  145. package/src/plugins/modal/image/services/image.upload.js +38 -4
  146. package/src/plugins/modal/link.js +9 -2
  147. package/src/plugins/modal/math.js +10 -2
  148. package/src/plugins/modal/video/index.js +112 -26
  149. package/src/plugins/modal/video/render/video.html.js +4 -1
  150. package/src/plugins/modal/video/services/video.size.js +15 -4
  151. package/src/plugins/modal/video/services/video.upload.js +10 -2
  152. package/src/plugins/popup/anchor.js +14 -2
  153. package/src/suneditor.js +8 -2
  154. package/src/themes/cobalt.css +12 -0
  155. package/src/themes/cream.css +12 -0
  156. package/src/themes/dark.css +12 -0
  157. package/src/themes/midnight.css +12 -0
  158. package/src/typedef.js +32 -21
  159. package/types/assets/icons/defaultIcons.d.ts +1 -0
  160. package/types/core/config/contextProvider.d.ts +7 -1
  161. package/types/core/config/eventManager.d.ts +37 -4
  162. package/types/core/config/optionProvider.d.ts +5 -1
  163. package/types/core/event/actions/index.d.ts +44 -6
  164. package/types/core/event/effects/keydown.registry.d.ts +37 -7
  165. package/types/core/event/effects/ruleHelpers.d.ts +6 -1
  166. package/types/core/event/eventOrchestrator.d.ts +13 -3
  167. package/types/core/event/handlers/handler_ww_clipboard.d.ts +15 -3
  168. package/types/core/event/handlers/handler_ww_dragDrop.d.ts +14 -2
  169. package/types/core/event/handlers/handler_ww_input.d.ts +10 -2
  170. package/types/core/event/handlers/handler_ww_key.d.ts +10 -2
  171. package/types/core/event/handlers/handler_ww_mouse.d.ts +25 -5
  172. package/types/core/event/ports.d.ts +11 -4
  173. package/types/core/event/reducers/keydown.reducer.d.ts +12 -3
  174. package/types/core/event/rules/keydown.rule.backspace.d.ts +6 -1
  175. package/types/core/kernel/store.d.ts +4 -1
  176. package/types/core/logic/dom/format.d.ts +32 -14
  177. package/types/core/logic/dom/html.d.ts +11 -3
  178. package/types/core/logic/dom/nodeTransform.d.ts +6 -6
  179. package/types/core/logic/panel/blockHandle.d.ts +64 -0
  180. package/types/core/logic/panel/blockResolver.d.ts +50 -0
  181. package/types/core/logic/panel/menu.d.ts +72 -3
  182. package/types/core/logic/shell/commandDispatcher.d.ts +8 -0
  183. package/types/core/logic/shell/component.d.ts +5 -3
  184. package/types/core/logic/shell/shortcuts.d.ts +10 -1
  185. package/types/core/logic/shell/ui.d.ts +7 -1
  186. package/types/core/schema/context.d.ts +7 -1
  187. package/types/core/schema/frameContext.d.ts +36 -4
  188. package/types/core/schema/options.d.ts +83 -2
  189. package/types/core/section/constructor.d.ts +38 -1
  190. package/types/events.d.ts +208 -32
  191. package/types/helper/converter.d.ts +4 -2
  192. package/types/helper/dom/domCheck.d.ts +12 -2
  193. package/types/helper/dom/domQuery.d.ts +35 -7
  194. package/types/helper/dom/domUtils.d.ts +4 -1
  195. package/types/interfaces/plugins.d.ts +2 -2
  196. package/types/langs/_Lang.d.ts +0 -1
  197. package/types/modules/contract/ColorPicker.d.ts +2 -2
  198. package/types/modules/contract/Controller.d.ts +7 -0
  199. package/types/modules/contract/Figure.d.ts +31 -4
  200. package/types/modules/ui/CommandMenu.d.ts +262 -0
  201. package/types/modules/ui/SelectMenu.d.ts +56 -3
  202. package/types/modules/ui/index.d.ts +1 -0
  203. package/types/plugins/browser/audioGallery.d.ts +1 -1
  204. package/types/plugins/browser/fileBrowser.d.ts +1 -1
  205. package/types/plugins/browser/fileGallery.d.ts +1 -1
  206. package/types/plugins/browser/imageGallery.d.ts +1 -1
  207. package/types/plugins/browser/videoGallery.d.ts +1 -1
  208. package/types/plugins/dropdown/align.d.ts +1 -1
  209. package/types/plugins/dropdown/blockStyle.d.ts +3 -1
  210. package/types/plugins/dropdown/table/index.d.ts +4 -1
  211. package/types/plugins/dropdown/table/services/table.clipboard.d.ts +5 -1
  212. package/types/plugins/dropdown/table/services/table.grid.d.ts +10 -2
  213. package/types/plugins/field/autocomplete.d.ts +0 -1
  214. package/types/plugins/field/slashCommand.d.ts +165 -0
  215. package/types/plugins/index.d.ts +3 -0
  216. package/types/plugins/modal/link.d.ts +4 -1
  217. package/types/typedef.d.ts +48 -23
@@ -1,5 +1,4 @@
1
1
  import { PluginDropdown } from '../../interfaces';
2
- import { dom } from '../../helper';
3
2
 
4
3
  /**
5
4
  * @typedef {Object} TemplatePluginOptions
@@ -32,11 +31,8 @@ class Template extends PluginDropdown {
32
31
  this.selectedIndex = -1;
33
32
  this.items = pluginOptions.items;
34
33
 
35
- // create HTML
36
- const menu = CreateHTML(this.items);
37
-
38
- // init
39
- this.$.menu.initDropdownTarget(Template, menu);
34
+ // create menu from items
35
+ this.$.menu.initDropdownTarget(Template, CreateItems(this.items));
40
36
  }
41
37
 
42
38
  /**
@@ -60,33 +56,21 @@ class Template extends PluginDropdown {
60
56
 
61
57
  /**
62
58
  * @param {Array<{name: string, html: string}>} templateList - Template items
63
- * @returns {HTMLElement}
59
+ * @returns {Array<import('../../core/logic/panel/menu').DropdownItem>}
64
60
  */
65
- function CreateHTML(templateList) {
61
+ function CreateItems(templateList) {
66
62
  if (!templateList || templateList.length === 0) {
67
- console.warn('[SUNEDITOR.plugins.template.warn] To use the "template" plugin, please define the "templates" option.');
68
- }
69
-
70
- let list = '<div class="se-dropdown se-list-inner"><ul class="se-list-basic">';
71
- for (let i = 0, len = (templateList || []).length, t; i < len; i++) {
72
- t = templateList[i];
73
- list += /*html*/ `
74
- <li>
75
- <button
76
- type="button"
77
- class="se-btn se-btn-list"
78
- data-command="template"
79
- data-value="${i}"
80
- title="${t.name}"
81
- aria-label="${t.name}"
82
- >
83
- ${t.name}
84
- </button>
85
- </li>`;
63
+ console.warn(
64
+ '[SUNEDITOR.plugins.template.warn] To use the "template" plugin, please define the "templates" option.',
65
+ );
86
66
  }
87
- list += '</ul></div>';
88
67
 
89
- return dom.utils.createElement('DIV', { class: 'se-list-layer' }, list);
68
+ return (templateList || []).map((t, i) => ({
69
+ command: 'template',
70
+ value: String(i),
71
+ title: t.name,
72
+ innerHTML: t.name,
73
+ }));
90
74
  }
91
75
 
92
76
  export default Template;
@@ -29,14 +29,13 @@ class TextStyle extends PluginDropdown {
29
29
  this.title = this.$.lang.textStyle;
30
30
  this.icon = 'text_style';
31
31
 
32
- // create HTML
33
- const menu = CreateHTML(this.$, pluginOptions.items);
32
+ // create menu from items
33
+ const menu = this.$.menu.initDropdownTarget(TextStyle, CreateItems(this.$, pluginOptions.items), {
34
+ className: 'se-list-format',
35
+ });
34
36
 
35
37
  // members
36
38
  this.styleList = menu.querySelectorAll('li button');
37
-
38
- // init
39
- this.$.menu.initDropdownTarget(TextStyle, menu);
40
39
  }
41
40
 
42
41
  /**
@@ -58,7 +57,11 @@ class TextStyle extends PluginDropdown {
58
57
  while (node && !this.$.format.isLine(node) && !this.$.component.is(node)) {
59
58
  if (node.nodeName.toLowerCase() === btn.getAttribute('data-command').toLowerCase()) {
60
59
  value = data[v];
61
- if (/^\./.test(value) ? dom.utils.hasClass(node, value.replace(/^\./, '')) : /** @type {HTMLElement} */ (node).style[value]) {
60
+ if (
61
+ /^\./.test(value)
62
+ ? dom.utils.hasClass(node, value.replace(/^\./, ''))
63
+ : /** @type {HTMLElement} */ (node).style[value]
64
+ ) {
62
65
  active = true;
63
66
  break;
64
67
  }
@@ -98,9 +101,9 @@ class TextStyle extends PluginDropdown {
98
101
  /**
99
102
  * @param {SunEditor.Deps} $ - Kernel dependencies
100
103
  * @param {Array<string|{name: string, class: string, tag?: string}>} [items] - Text style items
101
- * @returns {HTMLElement}
104
+ * @returns {Array<import('../../core/logic/panel/menu').DropdownItem>}
102
105
  */
103
- function CreateHTML({ lang }, items) {
106
+ function CreateItems({ lang }, items) {
104
107
  const defaultList = {
105
108
  code: {
106
109
  name: lang.menu_code,
@@ -114,13 +117,10 @@ function CreateHTML({ lang }, items) {
114
117
  },
115
118
  };
116
119
  const styleList = items || Object.keys(defaultList);
120
+ const result = [];
117
121
 
118
- let list = '<div class="se-list-inner"><ul class="se-list-basic">';
119
- for (let i = 0, len = styleList.length, t, tag, name, attrs, command, value; i < len; i++) {
122
+ for (let i = 0, len = styleList.length, t, tag, name, value; i < len; i++) {
120
123
  t = styleList[i];
121
- attrs = '';
122
- value = '';
123
- command = [];
124
124
 
125
125
  if (typeof t === 'string') {
126
126
  const cssText = defaultList[t.toLowerCase()];
@@ -130,30 +130,17 @@ function CreateHTML({ lang }, items) {
130
130
 
131
131
  name = t.name;
132
132
  tag = t.tag || 'span';
133
-
134
- attrs += ` class="${t.class}"`;
135
- value += `.${t.class.trim().replace(/\s+/g, ',.')}`;
136
- command.push('class');
137
-
138
- value = value.replace(/,$/, '');
139
-
140
- list += `
141
- <li>
142
- <button
143
- type="button"
144
- class="se-btn se-btn-list"
145
- data-command="${tag}"
146
- data-value="${value}"
147
- title="${name}"
148
- aria-label="${name}"
149
- >
150
- <${tag}${attrs}>${name}</${tag}>
151
- </button>
152
- </li>`;
133
+ value = `.${t.class.trim().replace(/\s+/g, ',.')}`.replace(/,$/, '');
134
+
135
+ result.push({
136
+ command: tag,
137
+ value,
138
+ title: name,
139
+ innerHTML: `<${tag} class="${t.class}">${name}</${tag}>`,
140
+ });
153
141
  }
154
- list += '</ul></div>';
155
142
 
156
- return dom.utils.createElement('DIV', { class: 'se-dropdown se-list-layer se-list-format' }, list);
143
+ return result;
157
144
  }
158
145
 
159
146
  export default TextStyle;
@@ -128,8 +128,6 @@ class Autocomplete extends PluginField {
128
128
  */
129
129
  constructor(kernel, pluginOptions) {
130
130
  super(kernel);
131
- this.title = this.$.lang.autocomplete;
132
- this.icon = 'autocomplete';
133
131
 
134
132
  // global defaults
135
133
  const limitSize = pluginOptions.limitSize || 5;
@@ -164,7 +162,11 @@ class Autocomplete extends PluginField {
164
162
 
165
163
  // controller
166
164
  const controllerEl = CreateHTML_controller();
167
- this.selectMenu = new SelectMenu(this.$, { position: 'right-bottom', dir: 'ltr', closeMethod: () => this.controller.close() });
165
+ this.selectMenu = new SelectMenu(this.$, {
166
+ position: 'right-bottom',
167
+ dir: 'ltr',
168
+ closeMethod: () => this.controller.close(),
169
+ });
168
170
  this.controller = new Controller(
169
171
  this,
170
172
  this.$,
@@ -263,7 +265,9 @@ class Autocomplete extends PluginField {
263
265
 
264
266
  if (!response) {
265
267
  if (ctx.directData) {
266
- response = ctx.directData.filter((item) => item.key.toLowerCase().startsWith(lowerValue)).slice(0, limit);
268
+ response = ctx.directData
269
+ .filter((item) => item.key.toLowerCase().startsWith(lowerValue))
270
+ .slice(0, limit);
267
271
  } else {
268
272
  const xmlHttp = await ctx.apiManager.asyncCall({ method: 'GET', url: this.#createUrl(ctx, value) });
269
273
  const json = JSON.parse(xmlHttp.responseText);
@@ -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;