suneditor 3.1.4 → 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.
- package/dist/suneditor-contents.min.css +1 -1
- package/dist/suneditor.min.css +28 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +2 -1
- package/src/assets/design/color.css +12 -0
- package/src/assets/design/size.css +9 -2
- package/src/assets/icons/defaultIcons.js +96 -47
- package/src/assets/suneditor-contents.css +3 -2
- package/src/assets/suneditor.css +480 -31
- package/src/core/config/contextProvider.js +2 -1
- package/src/core/config/eventManager.js +41 -2
- package/src/core/config/optionProvider.js +23 -2
- package/src/core/editor.js +32 -14
- package/src/core/event/actions/index.js +76 -9
- package/src/core/event/effects/common.registry.js +2 -1
- package/src/core/event/effects/keydown.registry.js +169 -33
- package/src/core/event/effects/ruleHelpers.js +20 -6
- package/src/core/event/eventOrchestrator.js +136 -23
- package/src/core/event/handlers/handler_toolbar.js +4 -2
- package/src/core/event/handlers/handler_ww_dragDrop.js +10 -0
- package/src/core/event/handlers/handler_ww_input.js +9 -2
- package/src/core/event/handlers/handler_ww_key.js +86 -17
- package/src/core/event/handlers/handler_ww_mouse.js +45 -8
- package/src/core/event/ports.js +23 -5
- package/src/core/event/reducers/keydown.reducer.js +15 -2
- package/src/core/event/rules/keydown.rule.arrow.js +12 -2
- package/src/core/event/rules/keydown.rule.backspace.js +110 -15
- package/src/core/event/rules/keydown.rule.delete.js +62 -7
- package/src/core/event/rules/keydown.rule.enter.js +62 -31
- package/src/core/event/support/defaultLineManager.js +9 -2
- package/src/core/event/support/selectionState.js +13 -3
- package/src/core/kernel/coreKernel.js +1 -0
- package/src/core/logic/dom/char.js +13 -3
- package/src/core/logic/dom/format.js +171 -39
- package/src/core/logic/dom/html.js +350 -72
- package/src/core/logic/dom/inline.js +229 -34
- package/src/core/logic/dom/listFormat.js +111 -20
- package/src/core/logic/dom/nodeTransform.js +36 -11
- package/src/core/logic/dom/offset.js +116 -31
- package/src/core/logic/dom/selection.js +147 -29
- package/src/core/logic/panel/blockHandle.js +785 -0
- package/src/core/logic/panel/blockResolver.js +278 -0
- package/src/core/logic/panel/finder.js +16 -6
- package/src/core/logic/panel/menu.js +119 -18
- package/src/core/logic/panel/toolbar.js +75 -19
- package/src/core/logic/panel/viewer.js +76 -20
- package/src/core/logic/shell/_commandExecutor.js +44 -9
- package/src/core/logic/shell/commandDispatcher.js +46 -4
- package/src/core/logic/shell/component.js +102 -25
- package/src/core/logic/shell/focusManager.js +13 -3
- package/src/core/logic/shell/history.js +21 -6
- package/src/core/logic/shell/pluginManager.js +21 -13
- package/src/core/logic/shell/shortcuts.js +9 -1
- package/src/core/logic/shell/ui.js +139 -37
- package/src/core/schema/frameContext.js +34 -3
- package/src/core/schema/options.js +46 -4
- package/src/core/section/constructor.js +537 -93
- package/src/core/section/documentType.js +35 -6
- package/src/helper/converter.js +24 -7
- package/src/helper/dom/domCheck.js +25 -5
- package/src/helper/dom/domQuery.js +2 -1
- package/src/helper/dom/domUtils.js +17 -4
- package/src/helper/env.js +11 -2
- package/src/helper/keyCodeMap.js +6 -2
- package/src/helper/markdown.js +24 -5
- package/src/helper/msOffice.js +9 -2
- package/src/interfaces/plugins.js +1 -1
- package/src/langs/ckb.js +0 -1
- package/src/langs/cs.js +0 -1
- package/src/langs/da.js +0 -1
- package/src/langs/de.js +0 -1
- package/src/langs/en.js +0 -1
- package/src/langs/es.js +0 -1
- package/src/langs/fa.js +0 -1
- package/src/langs/fr.js +0 -1
- package/src/langs/he.js +0 -1
- package/src/langs/hu.js +0 -1
- package/src/langs/index.js +27 -1
- package/src/langs/it.js +0 -1
- package/src/langs/ja.js +0 -1
- package/src/langs/km.js +0 -1
- package/src/langs/ko.js +0 -1
- package/src/langs/lv.js +0 -1
- package/src/langs/nl.js +0 -1
- package/src/langs/pl.js +0 -1
- package/src/langs/pt_br.js +0 -1
- package/src/langs/ro.js +0 -1
- package/src/langs/ru.js +0 -1
- package/src/langs/se.js +0 -1
- package/src/langs/tr.js +0 -1
- package/src/langs/uk.js +0 -1
- package/src/langs/ur.js +0 -1
- package/src/langs/zh_cn.js +0 -1
- package/src/modules/contract/Browser.js +58 -14
- package/src/modules/contract/ColorPicker.js +12 -4
- package/src/modules/contract/Controller.js +37 -7
- package/src/modules/contract/Figure.js +156 -36
- package/src/modules/contract/HueSlider.js +27 -4
- package/src/modules/contract/Modal.js +50 -10
- package/src/modules/manager/FileManager.js +50 -8
- package/src/modules/ui/CommandMenu.js +597 -0
- package/src/modules/ui/ModalAnchorEditor.js +33 -6
- package/src/modules/ui/SelectMenu.js +481 -36
- package/src/modules/ui/index.js +1 -0
- package/src/plugins/browser/audioGallery.js +2 -1
- package/src/plugins/browser/fileBrowser.js +9 -2
- package/src/plugins/browser/fileGallery.js +2 -1
- package/src/plugins/browser/imageGallery.js +18 -3
- package/src/plugins/browser/videoGallery.js +13 -4
- package/src/plugins/command/blockquote.js +6 -1
- package/src/plugins/command/codeBlock.js +43 -5
- package/src/plugins/command/exportPDF.js +20 -4
- package/src/plugins/command/fileUpload.js +46 -11
- package/src/plugins/command/list_bulleted.js +6 -1
- package/src/plugins/command/list_numbered.js +6 -1
- package/src/plugins/dropdown/align.js +21 -33
- package/src/plugins/dropdown/backgroundColor.js +10 -2
- package/src/plugins/dropdown/blockStyle.js +17 -22
- package/src/plugins/dropdown/font.js +34 -34
- package/src/plugins/dropdown/hr.js +15 -43
- package/src/plugins/dropdown/layout.js +10 -27
- package/src/plugins/dropdown/lineHeight.js +10 -33
- package/src/plugins/dropdown/list.js +19 -25
- package/src/plugins/dropdown/paragraphStyle.js +16 -24
- package/src/plugins/dropdown/table/index.js +72 -17
- package/src/plugins/dropdown/table/render/table.html.js +3 -1
- package/src/plugins/dropdown/table/services/table.cell.js +33 -7
- package/src/plugins/dropdown/table/services/table.clipboard.js +5 -1
- package/src/plugins/dropdown/table/services/table.grid.js +40 -9
- package/src/plugins/dropdown/table/services/table.resize.js +55 -11
- package/src/plugins/dropdown/table/services/table.selection.js +27 -7
- package/src/plugins/dropdown/table/services/table.style.js +120 -20
- package/src/plugins/dropdown/template.js +13 -29
- package/src/plugins/dropdown/textStyle.js +22 -35
- package/src/plugins/field/autocomplete.js +8 -4
- package/src/plugins/field/slashCommand.js +297 -0
- package/src/plugins/index.js +3 -0
- package/src/plugins/input/fontSize.js +37 -8
- package/src/plugins/input/pageNavigator.js +7 -1
- package/src/plugins/modal/audio.js +71 -17
- package/src/plugins/modal/drawing.js +41 -11
- package/src/plugins/modal/embed.js +92 -22
- package/src/plugins/modal/image/index.js +114 -28
- package/src/plugins/modal/image/services/image.size.js +9 -2
- package/src/plugins/modal/image/services/image.upload.js +38 -4
- package/src/plugins/modal/link.js +9 -2
- package/src/plugins/modal/math.js +10 -2
- package/src/plugins/modal/video/index.js +112 -26
- package/src/plugins/modal/video/render/video.html.js +4 -1
- package/src/plugins/modal/video/services/video.size.js +15 -4
- package/src/plugins/modal/video/services/video.upload.js +10 -2
- package/src/plugins/popup/anchor.js +14 -2
- package/src/suneditor.js +8 -2
- package/src/themes/cobalt.css +12 -0
- package/src/themes/cream.css +12 -0
- package/src/themes/dark.css +12 -0
- package/src/themes/midnight.css +12 -0
- package/src/typedef.js +32 -21
- package/types/assets/icons/defaultIcons.d.ts +1 -0
- package/types/core/config/contextProvider.d.ts +7 -1
- package/types/core/config/eventManager.d.ts +37 -4
- package/types/core/config/optionProvider.d.ts +5 -1
- package/types/core/event/actions/index.d.ts +44 -6
- package/types/core/event/effects/keydown.registry.d.ts +37 -7
- package/types/core/event/effects/ruleHelpers.d.ts +6 -1
- package/types/core/event/eventOrchestrator.d.ts +6 -1
- package/types/core/event/handlers/handler_ww_clipboard.d.ts +15 -3
- package/types/core/event/handlers/handler_ww_dragDrop.d.ts +14 -2
- package/types/core/event/handlers/handler_ww_input.d.ts +10 -2
- package/types/core/event/handlers/handler_ww_key.d.ts +10 -2
- package/types/core/event/handlers/handler_ww_mouse.d.ts +25 -5
- package/types/core/event/ports.d.ts +7 -2
- package/types/core/event/rules/keydown.rule.backspace.d.ts +6 -1
- package/types/core/kernel/store.d.ts +4 -1
- package/types/core/logic/dom/format.d.ts +32 -14
- package/types/core/logic/dom/html.d.ts +11 -3
- package/types/core/logic/dom/nodeTransform.d.ts +6 -6
- package/types/core/logic/panel/blockHandle.d.ts +64 -0
- package/types/core/logic/panel/blockResolver.d.ts +50 -0
- package/types/core/logic/panel/menu.d.ts +72 -3
- package/types/core/logic/shell/commandDispatcher.d.ts +8 -0
- package/types/core/logic/shell/component.d.ts +5 -3
- package/types/core/logic/shell/shortcuts.d.ts +10 -1
- package/types/core/logic/shell/ui.d.ts +7 -1
- package/types/core/schema/context.d.ts +7 -1
- package/types/core/schema/frameContext.d.ts +36 -4
- package/types/core/schema/options.d.ts +83 -2
- package/types/core/section/constructor.d.ts +38 -1
- package/types/events.d.ts +208 -32
- package/types/helper/converter.d.ts +4 -2
- package/types/helper/dom/domCheck.d.ts +12 -2
- package/types/helper/dom/domQuery.d.ts +35 -7
- package/types/helper/dom/domUtils.d.ts +4 -1
- package/types/interfaces/plugins.d.ts +2 -2
- package/types/langs/_Lang.d.ts +0 -1
- package/types/modules/contract/ColorPicker.d.ts +2 -2
- package/types/modules/contract/Controller.d.ts +7 -0
- package/types/modules/contract/Figure.d.ts +31 -4
- package/types/modules/ui/CommandMenu.d.ts +262 -0
- package/types/modules/ui/SelectMenu.d.ts +56 -3
- package/types/modules/ui/index.d.ts +1 -0
- package/types/plugins/browser/audioGallery.d.ts +1 -1
- package/types/plugins/browser/fileBrowser.d.ts +1 -1
- package/types/plugins/browser/fileGallery.d.ts +1 -1
- package/types/plugins/browser/imageGallery.d.ts +1 -1
- package/types/plugins/browser/videoGallery.d.ts +1 -1
- package/types/plugins/dropdown/align.d.ts +1 -1
- package/types/plugins/dropdown/blockStyle.d.ts +3 -1
- package/types/plugins/dropdown/table/index.d.ts +4 -1
- package/types/plugins/dropdown/table/services/table.clipboard.d.ts +5 -1
- package/types/plugins/dropdown/table/services/table.grid.d.ts +10 -2
- package/types/plugins/field/autocomplete.d.ts +0 -1
- package/types/plugins/field/slashCommand.d.ts +165 -0
- package/types/plugins/index.d.ts +3 -0
- package/types/plugins/modal/link.d.ts +4 -1
- 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
|
|
36
|
-
|
|
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 {
|
|
59
|
+
* @returns {Array<import('../../core/logic/panel/menu').DropdownItem>}
|
|
64
60
|
*/
|
|
65
|
-
function
|
|
61
|
+
function CreateItems(templateList) {
|
|
66
62
|
if (!templateList || templateList.length === 0) {
|
|
67
|
-
console.warn(
|
|
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
|
|
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
|
|
33
|
-
const menu =
|
|
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 (
|
|
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 {
|
|
104
|
+
* @returns {Array<import('../../core/logic/panel/menu').DropdownItem>}
|
|
102
105
|
*/
|
|
103
|
-
function
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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.$, {
|
|
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
|
|
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;
|
package/src/plugins/index.js
CHANGED
|
@@ -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)
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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', {
|
|
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 =
|
|
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(
|
|
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} */ (
|
|
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;
|