suneditor 3.0.0-beta.2 → 3.0.0-beta.20
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/CONTRIBUTING.md +186 -184
- package/LICENSE +21 -21
- package/README.md +157 -180
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +126 -123
- package/src/assets/design/color.css +131 -121
- package/src/assets/design/index.css +3 -3
- package/src/assets/design/size.css +37 -35
- package/src/assets/design/typography.css +37 -37
- package/src/assets/icons/defaultIcons.js +247 -232
- package/src/assets/suneditor-contents.css +779 -778
- package/src/assets/suneditor.css +43 -35
- package/src/core/base/eventHandlers/handler_toolbar.js +135 -135
- package/src/core/base/eventHandlers/handler_ww_clipboard.js +56 -56
- package/src/core/base/eventHandlers/handler_ww_dragDrop.js +115 -113
- package/src/core/base/eventHandlers/handler_ww_key_input.js +1200 -1200
- package/src/core/base/eventHandlers/handler_ww_mouse.js +194 -194
- package/src/core/base/eventManager.js +1550 -1484
- package/src/core/base/history.js +355 -355
- package/src/core/class/char.js +163 -162
- package/src/core/class/component.js +856 -842
- package/src/core/class/format.js +3433 -3422
- package/src/core/class/html.js +1927 -1890
- package/src/core/class/menu.js +357 -346
- package/src/core/class/nodeTransform.js +424 -424
- package/src/core/class/offset.js +858 -891
- package/src/core/class/selection.js +710 -620
- package/src/core/class/shortcuts.js +98 -98
- package/src/core/class/toolbar.js +438 -430
- package/src/core/class/ui.js +424 -422
- package/src/core/class/viewer.js +750 -750
- package/src/core/editor.js +1810 -1708
- package/src/core/section/actives.js +268 -241
- package/src/core/section/constructor.js +1348 -1661
- package/src/core/section/context.js +102 -102
- package/src/core/section/documentType.js +582 -561
- package/src/core/section/options.js +367 -0
- package/src/core/util/instanceCheck.js +59 -0
- package/src/editorInjector/_classes.js +36 -36
- package/src/editorInjector/_core.js +92 -92
- package/src/editorInjector/index.js +75 -75
- package/src/events.js +634 -622
- package/src/helper/clipboard.js +59 -59
- package/src/helper/converter.js +586 -564
- package/src/helper/dom/domCheck.js +304 -304
- package/src/helper/dom/domQuery.js +677 -669
- package/src/helper/dom/domUtils.js +618 -557
- package/src/helper/dom/index.js +12 -12
- package/src/helper/env.js +249 -240
- package/src/helper/index.js +25 -25
- package/src/helper/keyCodeMap.js +183 -183
- package/src/helper/numbers.js +72 -72
- package/src/helper/unicode.js +47 -47
- package/src/langs/ckb.js +231 -231
- package/src/langs/cs.js +231 -231
- package/src/langs/da.js +231 -231
- package/src/langs/de.js +231 -231
- package/src/langs/en.js +230 -230
- package/src/langs/es.js +231 -231
- package/src/langs/fa.js +231 -231
- package/src/langs/fr.js +231 -231
- package/src/langs/he.js +231 -231
- package/src/langs/hu.js +230 -230
- package/src/langs/index.js +28 -28
- package/src/langs/it.js +231 -231
- package/src/langs/ja.js +230 -230
- package/src/langs/km.js +230 -230
- package/src/langs/ko.js +230 -230
- package/src/langs/lv.js +231 -231
- package/src/langs/nl.js +231 -231
- package/src/langs/pl.js +231 -231
- package/src/langs/pt_br.js +231 -231
- package/src/langs/ro.js +231 -231
- package/src/langs/ru.js +231 -231
- package/src/langs/se.js +231 -231
- package/src/langs/tr.js +231 -231
- package/src/langs/uk.js +231 -231
- package/src/langs/ur.js +231 -231
- package/src/langs/zh_cn.js +231 -231
- package/src/modules/ApiManager.js +191 -191
- package/src/modules/Browser.js +669 -667
- package/src/modules/ColorPicker.js +364 -362
- package/src/modules/Controller.js +474 -454
- package/src/modules/Figure.js +1620 -1617
- package/src/modules/FileManager.js +359 -359
- package/src/modules/HueSlider.js +577 -565
- package/src/modules/Modal.js +346 -346
- package/src/modules/ModalAnchorEditor.js +643 -643
- package/src/modules/SelectMenu.js +549 -549
- package/src/modules/_DragHandle.js +17 -17
- package/src/modules/index.js +14 -14
- package/src/plugins/browser/audioGallery.js +83 -83
- package/src/plugins/browser/fileBrowser.js +103 -103
- package/src/plugins/browser/fileGallery.js +83 -83
- package/src/plugins/browser/imageGallery.js +81 -81
- package/src/plugins/browser/videoGallery.js +103 -103
- package/src/plugins/command/blockquote.js +61 -60
- package/src/plugins/command/exportPDF.js +134 -134
- package/src/plugins/command/fileUpload.js +456 -456
- package/src/plugins/command/list_bulleted.js +149 -148
- package/src/plugins/command/list_numbered.js +152 -151
- package/src/plugins/dropdown/align.js +157 -155
- package/src/plugins/dropdown/backgroundColor.js +108 -104
- package/src/plugins/dropdown/font.js +141 -137
- package/src/plugins/dropdown/fontColor.js +109 -105
- package/src/plugins/dropdown/formatBlock.js +170 -178
- package/src/plugins/dropdown/hr.js +152 -152
- package/src/plugins/dropdown/layout.js +83 -83
- package/src/plugins/dropdown/lineHeight.js +131 -130
- package/src/plugins/dropdown/list.js +123 -122
- package/src/plugins/dropdown/paragraphStyle.js +138 -138
- package/src/plugins/dropdown/table.js +4110 -4000
- package/src/plugins/dropdown/template.js +83 -83
- package/src/plugins/dropdown/textStyle.js +149 -149
- package/src/plugins/field/mention.js +242 -242
- package/src/plugins/index.js +120 -120
- package/src/plugins/input/fontSize.js +414 -410
- package/src/plugins/input/pageNavigator.js +71 -70
- package/src/plugins/modal/audio.js +677 -677
- package/src/plugins/modal/drawing.js +537 -531
- package/src/plugins/modal/embed.js +886 -886
- package/src/plugins/modal/image.js +1377 -1376
- package/src/plugins/modal/link.js +248 -240
- package/src/plugins/modal/math.js +563 -563
- package/src/plugins/modal/video.js +1226 -1226
- package/src/plugins/popup/anchor.js +224 -222
- package/src/suneditor.js +114 -107
- package/src/themes/dark.css +132 -122
- package/src/typedef.js +132 -130
- package/types/assets/icons/defaultIcons.d.ts +8 -0
- package/types/core/base/eventManager.d.ts +29 -4
- package/types/core/class/char.d.ts +2 -1
- package/types/core/class/component.d.ts +1 -2
- package/types/core/class/format.d.ts +8 -1
- package/types/core/class/html.d.ts +8 -0
- package/types/core/class/menu.d.ts +8 -0
- package/types/core/class/offset.d.ts +24 -26
- package/types/core/class/selection.d.ts +2 -0
- package/types/core/class/toolbar.d.ts +6 -0
- package/types/core/class/ui.d.ts +1 -1
- package/types/core/editor.d.ts +34 -12
- package/types/core/section/constructor.d.ts +5 -638
- package/types/core/section/documentType.d.ts +12 -2
- package/types/core/section/options.d.ts +740 -0
- package/types/core/util/instanceCheck.d.ts +50 -0
- package/types/editorInjector/_core.d.ts +5 -5
- package/types/editorInjector/index.d.ts +2 -2
- package/types/events.d.ts +2 -0
- package/types/helper/converter.d.ts +9 -0
- package/types/helper/dom/domQuery.d.ts +5 -5
- package/types/helper/dom/domUtils.d.ts +8 -0
- package/types/helper/env.d.ts +6 -1
- package/types/helper/index.d.ts +4 -1
- package/types/index.d.ts +122 -120
- package/types/langs/_Lang.d.ts +194 -194
- package/types/modules/ColorPicker.d.ts +5 -1
- package/types/modules/Controller.d.ts +8 -4
- package/types/modules/Figure.d.ts +2 -1
- package/types/modules/HueSlider.d.ts +4 -1
- package/types/modules/SelectMenu.d.ts +1 -1
- package/types/plugins/command/blockquote.d.ts +1 -0
- package/types/plugins/command/list_bulleted.d.ts +1 -0
- package/types/plugins/command/list_numbered.d.ts +1 -0
- package/types/plugins/dropdown/align.d.ts +1 -0
- package/types/plugins/dropdown/backgroundColor.d.ts +1 -0
- package/types/plugins/dropdown/font.d.ts +1 -0
- package/types/plugins/dropdown/fontColor.d.ts +1 -0
- package/types/plugins/dropdown/formatBlock.d.ts +3 -2
- package/types/plugins/dropdown/lineHeight.d.ts +1 -0
- package/types/plugins/dropdown/list.d.ts +1 -0
- package/types/plugins/dropdown/table.d.ts +6 -0
- package/types/plugins/input/fontSize.d.ts +1 -0
- package/types/plugins/modal/drawing.d.ts +4 -0
- package/types/plugins/modal/link.d.ts +32 -15
- package/types/suneditor.d.ts +13 -9
- package/types/typedef.d.ts +8 -0
|
@@ -1,242 +1,242 @@
|
|
|
1
|
-
import EditorInjector from '../../editorInjector';
|
|
2
|
-
import { ApiManager, SelectMenu, Controller } from '../../modules';
|
|
3
|
-
import { dom, converter } from '../../helper';
|
|
4
|
-
|
|
5
|
-
const { debounce } = converter;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @class
|
|
9
|
-
* @description Mention Plugin
|
|
10
|
-
* - A plugin that provides a mention feature using `@` or a custom trigger character.
|
|
11
|
-
* - Displays a mention list when the trigger character is typed.
|
|
12
|
-
* - Supports fetching mention data from an API or a predefined data array.
|
|
13
|
-
* - Uses caching for optimized performance.
|
|
14
|
-
*/
|
|
15
|
-
class Mention extends EditorInjector {
|
|
16
|
-
static key = 'mention';
|
|
17
|
-
static type = 'field';
|
|
18
|
-
static className = '';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @constructor
|
|
22
|
-
* @param {__se__EditorCore} editor - The root editor instance
|
|
23
|
-
* @param {Object} pluginOptions
|
|
24
|
-
* @param {string=} [pluginOptions.triggerText="@"] The character that triggers the mention list. Default is '@'.
|
|
25
|
-
* @param {number=} [pluginOptions.limitSize=5] The number of items to display in the mention list. Default is 5.
|
|
26
|
-
* @param {number=} [pluginOptions.searchStartLength=0] The number of characters to start searching for the mention list. Default is 0.
|
|
27
|
-
* @param {number=} [pluginOptions.delayTime=200] The time to wait before displaying the mention list. Default is 200ms.
|
|
28
|
-
* @param {Array<{key: string, name: string, url: string}>=} pluginOptions.data Use data without using API.
|
|
29
|
-
* @param {string=} pluginOptions.apiUrl The URL to call the mention list. Default is ''.
|
|
30
|
-
* @param {Object<string, string>=} pluginOptions.apiHeaders The headers to send with the API call. Default is {}.
|
|
31
|
-
* @param {boolean=} [pluginOptions.useCachingData=true] Whether to cache the mention list data. Default is true.
|
|
32
|
-
* @param {boolean=} [pluginOptions.useCachingFieldData=true] Whether to cache the mention list data in the field. Default is true.
|
|
33
|
-
*/
|
|
34
|
-
constructor(editor, pluginOptions) {
|
|
35
|
-
super(editor);
|
|
36
|
-
// plugin basic properties
|
|
37
|
-
this.title = this.lang.mention;
|
|
38
|
-
this.icon = 'mention';
|
|
39
|
-
|
|
40
|
-
// members
|
|
41
|
-
this.triggerText = pluginOptions.triggerText || '@';
|
|
42
|
-
this.limitSize = pluginOptions.limitSize || 5;
|
|
43
|
-
this.searchStartLength = pluginOptions.searchStartLength || 0;
|
|
44
|
-
this.delayTime = typeof pluginOptions.delayTime === 'number' ? pluginOptions.delayTime : 200;
|
|
45
|
-
this.directData = pluginOptions.data;
|
|
46
|
-
this.apiUrl = pluginOptions.apiUrl?.replace(/\s/g, '').replace(/\{limitSize\}/i, String(this.limitSize)) || '';
|
|
47
|
-
this._delay = 0;
|
|
48
|
-
this._lastAtPos = 0;
|
|
49
|
-
this._anchorOffset = 0;
|
|
50
|
-
this._anchorNode = null;
|
|
51
|
-
// members - api, caching
|
|
52
|
-
this.apiManager = new ApiManager(this, { headers: pluginOptions.apiHeaders });
|
|
53
|
-
this.cachingData = pluginOptions.useCachingData ?? true ? new Map() : null;
|
|
54
|
-
this.cachingFieldData = pluginOptions.useCachingFieldData ?? true ? [] : null;
|
|
55
|
-
|
|
56
|
-
// controller
|
|
57
|
-
const controllerEl = CreateHTML_controller();
|
|
58
|
-
this.selectMenu = new SelectMenu(this, { position: 'right-bottom', dir: 'ltr', closeMethod: () => this.controller.close() });
|
|
59
|
-
this.controller = new Controller(
|
|
60
|
-
this,
|
|
61
|
-
controllerEl,
|
|
62
|
-
{
|
|
63
|
-
position: 'bottom',
|
|
64
|
-
initMethod: () => {
|
|
65
|
-
this.apiManager.cancel();
|
|
66
|
-
this.selectMenu.close();
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
null
|
|
70
|
-
);
|
|
71
|
-
this.selectMenu.on(controllerEl.firstElementChild, this.#SelectMention.bind(this));
|
|
72
|
-
|
|
73
|
-
// onInput debounce
|
|
74
|
-
this.onInput = debounce(this.onInput.bind(this), this.delayTime);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @editorMethod Editor.EventManager
|
|
79
|
-
* @description Executes the event function of "input".
|
|
80
|
-
* @returns {Promise<boolean>}
|
|
81
|
-
*/
|
|
82
|
-
async onInput() {
|
|
83
|
-
if (!this.directData) {
|
|
84
|
-
this.apiManager.cancel();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const sel = this.selection.get();
|
|
88
|
-
if (!sel.rangeCount) {
|
|
89
|
-
this.selectMenu.close();
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const anchorNode = sel.anchorNode;
|
|
94
|
-
const anchorOffset = sel.anchorOffset;
|
|
95
|
-
const textBeforeCursor = anchorNode.textContent.substring(0, anchorOffset);
|
|
96
|
-
const lastAtPos = textBeforeCursor.lastIndexOf(this.triggerText);
|
|
97
|
-
|
|
98
|
-
if (lastAtPos > -1) {
|
|
99
|
-
const mentionQuery = textBeforeCursor.substring(lastAtPos + 1, anchorOffset);
|
|
100
|
-
const beforeText = textBeforeCursor[lastAtPos - 1]?.trim();
|
|
101
|
-
if (!/\s/.test(mentionQuery) && (!beforeText || dom.check.isZeroWidth(beforeText))) {
|
|
102
|
-
if (mentionQuery.length < this.searchStartLength) return true;
|
|
103
|
-
|
|
104
|
-
const anchorParent = anchorNode.parentNode;
|
|
105
|
-
if (dom.check.isAnchor(anchorParent) && !anchorParent.getAttribute('data-se-mention')) {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const result = await this._createMentionList(mentionQuery, anchorNode);
|
|
111
|
-
this._lastAtPos = lastAtPos;
|
|
112
|
-
this._anchorNode = anchorNode;
|
|
113
|
-
this._anchorOffset = anchorOffset;
|
|
114
|
-
return !result;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
console.warn('[SUNEDITOR.mention.api.file] ', error);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this.selectMenu.close();
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @private
|
|
127
|
-
* @description Generates the mention list based on user input.
|
|
128
|
-
* - Fetches data from cache, direct data, or an API.
|
|
129
|
-
* - Creates and opens the mention dropdown.
|
|
130
|
-
* - Caches the fetched data for future use.
|
|
131
|
-
* @param {string} value - The mention query text.
|
|
132
|
-
* @param {Node} targetNode - The node where the mention is triggered.
|
|
133
|
-
* @returns {Promise<boolean>} - Returns `true` if the mention list is displayed, `false` otherwise.
|
|
134
|
-
*/
|
|
135
|
-
async _createMentionList(value, targetNode) {
|
|
136
|
-
const limit = this.limitSize;
|
|
137
|
-
const lowerValue = value.toLowerCase();
|
|
138
|
-
let response = null;
|
|
139
|
-
if (this.cachingData) {
|
|
140
|
-
response = this.cachingData.get(value);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (!response) {
|
|
144
|
-
if (this.directData) {
|
|
145
|
-
response = this.directData.filter((item) => item.key.toLowerCase().startsWith(lowerValue)).slice(0, limit);
|
|
146
|
-
} else {
|
|
147
|
-
const xmlHttp = await this.apiManager.asyncCall({ method: 'GET', url: this._createUrl(value) });
|
|
148
|
-
response = JSON.parse(xmlHttp.responseText);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (this.cachingFieldData) {
|
|
153
|
-
const uniqueKeys = new Set();
|
|
154
|
-
response = this.cachingFieldData
|
|
155
|
-
.concat(response)
|
|
156
|
-
.filter(({ key }) => {
|
|
157
|
-
if (uniqueKeys.has(key)) return false;
|
|
158
|
-
uniqueKeys.add(key);
|
|
159
|
-
return key.toLowerCase().startsWith(lowerValue);
|
|
160
|
-
})
|
|
161
|
-
.slice(0, limit);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!response?.length) {
|
|
165
|
-
this.selectMenu.close();
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const list = [];
|
|
170
|
-
const menus = [];
|
|
171
|
-
for (let i = 0, len = response.length, v; i < len; i++) {
|
|
172
|
-
v = response[i];
|
|
173
|
-
list.push(v);
|
|
174
|
-
menus.push(`<div class="se-mention-item"><span>${v.key}</span><span>${v.name}</span></div>`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (list.length === 0) {
|
|
178
|
-
this.selectMenu.close();
|
|
179
|
-
return false;
|
|
180
|
-
} else {
|
|
181
|
-
// controller open
|
|
182
|
-
this.controller.open(targetNode, null, { isWWTarget: true, initMethod: null, addOffset: null });
|
|
183
|
-
// select menu create
|
|
184
|
-
this.selectMenu.create(list, menus);
|
|
185
|
-
this.selectMenu.open();
|
|
186
|
-
this.selectMenu.setItem(0);
|
|
187
|
-
if (this.cachingData) this.cachingData.set(value, list);
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* @private
|
|
194
|
-
* @description Constructs the API request URL with the mention query.
|
|
195
|
-
* @param {string} key - The mention query text.
|
|
196
|
-
* @returns {string} - The formatted API request URL.
|
|
197
|
-
*/
|
|
198
|
-
_createUrl(key) {
|
|
199
|
-
return this.apiUrl.replace(/\{key\}/i, key);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* @description Inserts a mention link into the editor when a user selects a mention from the list.
|
|
204
|
-
* @param {{ key: string, name: string, url: string }} item - The selected mention item.
|
|
205
|
-
* @returns {boolean} - Returns `false` if insertion fails, otherwise completes execution.
|
|
206
|
-
*/
|
|
207
|
-
#SelectMention(item) {
|
|
208
|
-
if (!item) return false;
|
|
209
|
-
|
|
210
|
-
let oA = null;
|
|
211
|
-
const { key, name, url } = item;
|
|
212
|
-
const anchorParent = this._anchorNode.parentNode;
|
|
213
|
-
|
|
214
|
-
if (dom.check.isAnchor(anchorParent)) {
|
|
215
|
-
oA = anchorParent;
|
|
216
|
-
oA.setAttribute('data-se-mention', key);
|
|
217
|
-
oA.setAttribute('href', url);
|
|
218
|
-
oA.setAttribute('title', name);
|
|
219
|
-
oA.textContent = this.triggerText + key;
|
|
220
|
-
} else {
|
|
221
|
-
this.selection.setRange(this._anchorNode, this._lastAtPos, this._anchorNode, this._anchorOffset);
|
|
222
|
-
oA = dom.utils.createElement('A', { 'data-se-mention': key, href: url, title: name, target: '_blank' }, this.triggerText + key);
|
|
223
|
-
if (!this.html.insertNode(oA, { afterNode: null, skipCharCount: false })) return false;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
this.selectMenu.close();
|
|
227
|
-
|
|
228
|
-
const space = dom.utils.createTextNode('\u00A0');
|
|
229
|
-
oA.parentNode.insertBefore(space, oA.nextSibling);
|
|
230
|
-
this.selection.setRange(space, 1, space, 1);
|
|
231
|
-
|
|
232
|
-
if (this.cachingFieldData && !this.cachingFieldData.some((data) => data.key === item.key)) {
|
|
233
|
-
this.cachingFieldData.push(item);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function CreateHTML_controller() {
|
|
239
|
-
return dom.utils.createElement('DIV', { class: 'se-controller se-empty-controller' }, '<div></div>');
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export default Mention;
|
|
1
|
+
import EditorInjector from '../../editorInjector';
|
|
2
|
+
import { ApiManager, SelectMenu, Controller } from '../../modules';
|
|
3
|
+
import { dom, converter } from '../../helper';
|
|
4
|
+
|
|
5
|
+
const { debounce } = converter;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @class
|
|
9
|
+
* @description Mention Plugin
|
|
10
|
+
* - A plugin that provides a mention feature using `@` or a custom trigger character.
|
|
11
|
+
* - Displays a mention list when the trigger character is typed.
|
|
12
|
+
* - Supports fetching mention data from an API or a predefined data array.
|
|
13
|
+
* - Uses caching for optimized performance.
|
|
14
|
+
*/
|
|
15
|
+
class Mention extends EditorInjector {
|
|
16
|
+
static key = 'mention';
|
|
17
|
+
static type = 'field';
|
|
18
|
+
static className = '';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @constructor
|
|
22
|
+
* @param {__se__EditorCore} editor - The root editor instance
|
|
23
|
+
* @param {Object} pluginOptions
|
|
24
|
+
* @param {string=} [pluginOptions.triggerText="@"] The character that triggers the mention list. Default is '@'.
|
|
25
|
+
* @param {number=} [pluginOptions.limitSize=5] The number of items to display in the mention list. Default is 5.
|
|
26
|
+
* @param {number=} [pluginOptions.searchStartLength=0] The number of characters to start searching for the mention list. Default is 0.
|
|
27
|
+
* @param {number=} [pluginOptions.delayTime=200] The time to wait before displaying the mention list. Default is 200ms.
|
|
28
|
+
* @param {Array<{key: string, name: string, url: string}>=} pluginOptions.data Use data without using API.
|
|
29
|
+
* @param {string=} pluginOptions.apiUrl The URL to call the mention list. Default is ''.
|
|
30
|
+
* @param {Object<string, string>=} pluginOptions.apiHeaders The headers to send with the API call. Default is {}.
|
|
31
|
+
* @param {boolean=} [pluginOptions.useCachingData=true] Whether to cache the mention list data. Default is true.
|
|
32
|
+
* @param {boolean=} [pluginOptions.useCachingFieldData=true] Whether to cache the mention list data in the field. Default is true.
|
|
33
|
+
*/
|
|
34
|
+
constructor(editor, pluginOptions) {
|
|
35
|
+
super(editor);
|
|
36
|
+
// plugin basic properties
|
|
37
|
+
this.title = this.lang.mention;
|
|
38
|
+
this.icon = 'mention';
|
|
39
|
+
|
|
40
|
+
// members
|
|
41
|
+
this.triggerText = pluginOptions.triggerText || '@';
|
|
42
|
+
this.limitSize = pluginOptions.limitSize || 5;
|
|
43
|
+
this.searchStartLength = pluginOptions.searchStartLength || 0;
|
|
44
|
+
this.delayTime = typeof pluginOptions.delayTime === 'number' ? pluginOptions.delayTime : 200;
|
|
45
|
+
this.directData = pluginOptions.data;
|
|
46
|
+
this.apiUrl = pluginOptions.apiUrl?.replace(/\s/g, '').replace(/\{limitSize\}/i, String(this.limitSize)) || '';
|
|
47
|
+
this._delay = 0;
|
|
48
|
+
this._lastAtPos = 0;
|
|
49
|
+
this._anchorOffset = 0;
|
|
50
|
+
this._anchorNode = null;
|
|
51
|
+
// members - api, caching
|
|
52
|
+
this.apiManager = new ApiManager(this, { headers: pluginOptions.apiHeaders });
|
|
53
|
+
this.cachingData = pluginOptions.useCachingData ?? true ? new Map() : null;
|
|
54
|
+
this.cachingFieldData = pluginOptions.useCachingFieldData ?? true ? [] : null;
|
|
55
|
+
|
|
56
|
+
// controller
|
|
57
|
+
const controllerEl = CreateHTML_controller();
|
|
58
|
+
this.selectMenu = new SelectMenu(this, { position: 'right-bottom', dir: 'ltr', closeMethod: () => this.controller.close() });
|
|
59
|
+
this.controller = new Controller(
|
|
60
|
+
this,
|
|
61
|
+
controllerEl,
|
|
62
|
+
{
|
|
63
|
+
position: 'bottom',
|
|
64
|
+
initMethod: () => {
|
|
65
|
+
this.apiManager.cancel();
|
|
66
|
+
this.selectMenu.close();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
null
|
|
70
|
+
);
|
|
71
|
+
this.selectMenu.on(controllerEl.firstElementChild, this.#SelectMention.bind(this));
|
|
72
|
+
|
|
73
|
+
// onInput debounce
|
|
74
|
+
this.onInput = debounce(this.onInput.bind(this), this.delayTime);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @editorMethod Editor.EventManager
|
|
79
|
+
* @description Executes the event function of "input".
|
|
80
|
+
* @returns {Promise<boolean>}
|
|
81
|
+
*/
|
|
82
|
+
async onInput() {
|
|
83
|
+
if (!this.directData) {
|
|
84
|
+
this.apiManager.cancel();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sel = this.selection.get();
|
|
88
|
+
if (!sel.rangeCount) {
|
|
89
|
+
this.selectMenu.close();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const anchorNode = sel.anchorNode;
|
|
94
|
+
const anchorOffset = sel.anchorOffset;
|
|
95
|
+
const textBeforeCursor = anchorNode.textContent.substring(0, anchorOffset);
|
|
96
|
+
const lastAtPos = textBeforeCursor.lastIndexOf(this.triggerText);
|
|
97
|
+
|
|
98
|
+
if (lastAtPos > -1) {
|
|
99
|
+
const mentionQuery = textBeforeCursor.substring(lastAtPos + 1, anchorOffset);
|
|
100
|
+
const beforeText = textBeforeCursor[lastAtPos - 1]?.trim();
|
|
101
|
+
if (!/\s/.test(mentionQuery) && (!beforeText || dom.check.isZeroWidth(beforeText))) {
|
|
102
|
+
if (mentionQuery.length < this.searchStartLength) return true;
|
|
103
|
+
|
|
104
|
+
const anchorParent = anchorNode.parentNode;
|
|
105
|
+
if (dom.check.isAnchor(anchorParent) && !anchorParent.getAttribute('data-se-mention')) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await this._createMentionList(mentionQuery, anchorNode);
|
|
111
|
+
this._lastAtPos = lastAtPos;
|
|
112
|
+
this._anchorNode = anchorNode;
|
|
113
|
+
this._anchorOffset = anchorOffset;
|
|
114
|
+
return !result;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn('[SUNEDITOR.mention.api.file] ', error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.selectMenu.close();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @private
|
|
127
|
+
* @description Generates the mention list based on user input.
|
|
128
|
+
* - Fetches data from cache, direct data, or an API.
|
|
129
|
+
* - Creates and opens the mention dropdown.
|
|
130
|
+
* - Caches the fetched data for future use.
|
|
131
|
+
* @param {string} value - The mention query text.
|
|
132
|
+
* @param {Node} targetNode - The node where the mention is triggered.
|
|
133
|
+
* @returns {Promise<boolean>} - Returns `true` if the mention list is displayed, `false` otherwise.
|
|
134
|
+
*/
|
|
135
|
+
async _createMentionList(value, targetNode) {
|
|
136
|
+
const limit = this.limitSize;
|
|
137
|
+
const lowerValue = value.toLowerCase();
|
|
138
|
+
let response = null;
|
|
139
|
+
if (this.cachingData) {
|
|
140
|
+
response = this.cachingData.get(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!response) {
|
|
144
|
+
if (this.directData) {
|
|
145
|
+
response = this.directData.filter((item) => item.key.toLowerCase().startsWith(lowerValue)).slice(0, limit);
|
|
146
|
+
} else {
|
|
147
|
+
const xmlHttp = await this.apiManager.asyncCall({ method: 'GET', url: this._createUrl(value) });
|
|
148
|
+
response = JSON.parse(xmlHttp.responseText);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.cachingFieldData) {
|
|
153
|
+
const uniqueKeys = new Set();
|
|
154
|
+
response = this.cachingFieldData
|
|
155
|
+
.concat(response)
|
|
156
|
+
.filter(({ key }) => {
|
|
157
|
+
if (uniqueKeys.has(key)) return false;
|
|
158
|
+
uniqueKeys.add(key);
|
|
159
|
+
return key.toLowerCase().startsWith(lowerValue);
|
|
160
|
+
})
|
|
161
|
+
.slice(0, limit);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!response?.length) {
|
|
165
|
+
this.selectMenu.close();
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const list = [];
|
|
170
|
+
const menus = [];
|
|
171
|
+
for (let i = 0, len = response.length, v; i < len; i++) {
|
|
172
|
+
v = response[i];
|
|
173
|
+
list.push(v);
|
|
174
|
+
menus.push(`<div class="se-mention-item"><span>${v.key}</span><span>${v.name}</span></div>`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (list.length === 0) {
|
|
178
|
+
this.selectMenu.close();
|
|
179
|
+
return false;
|
|
180
|
+
} else {
|
|
181
|
+
// controller open
|
|
182
|
+
this.controller.open(targetNode, null, { isWWTarget: true, initMethod: null, addOffset: null });
|
|
183
|
+
// select menu create
|
|
184
|
+
this.selectMenu.create(list, menus);
|
|
185
|
+
this.selectMenu.open();
|
|
186
|
+
this.selectMenu.setItem(0);
|
|
187
|
+
if (this.cachingData) this.cachingData.set(value, list);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @private
|
|
194
|
+
* @description Constructs the API request URL with the mention query.
|
|
195
|
+
* @param {string} key - The mention query text.
|
|
196
|
+
* @returns {string} - The formatted API request URL.
|
|
197
|
+
*/
|
|
198
|
+
_createUrl(key) {
|
|
199
|
+
return this.apiUrl.replace(/\{key\}/i, key);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @description Inserts a mention link into the editor when a user selects a mention from the list.
|
|
204
|
+
* @param {{ key: string, name: string, url: string }} item - The selected mention item.
|
|
205
|
+
* @returns {boolean} - Returns `false` if insertion fails, otherwise completes execution.
|
|
206
|
+
*/
|
|
207
|
+
#SelectMention(item) {
|
|
208
|
+
if (!item) return false;
|
|
209
|
+
|
|
210
|
+
let oA = null;
|
|
211
|
+
const { key, name, url } = item;
|
|
212
|
+
const anchorParent = this._anchorNode.parentNode;
|
|
213
|
+
|
|
214
|
+
if (dom.check.isAnchor(anchorParent)) {
|
|
215
|
+
oA = anchorParent;
|
|
216
|
+
oA.setAttribute('data-se-mention', key);
|
|
217
|
+
oA.setAttribute('href', url);
|
|
218
|
+
oA.setAttribute('title', name);
|
|
219
|
+
oA.textContent = this.triggerText + key;
|
|
220
|
+
} else {
|
|
221
|
+
this.selection.setRange(this._anchorNode, this._lastAtPos, this._anchorNode, this._anchorOffset);
|
|
222
|
+
oA = dom.utils.createElement('A', { 'data-se-mention': key, href: url, title: name, target: '_blank' }, this.triggerText + key);
|
|
223
|
+
if (!this.html.insertNode(oA, { afterNode: null, skipCharCount: false })) return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.selectMenu.close();
|
|
227
|
+
|
|
228
|
+
const space = dom.utils.createTextNode('\u00A0');
|
|
229
|
+
oA.parentNode.insertBefore(space, oA.nextSibling);
|
|
230
|
+
this.selection.setRange(space, 1, space, 1);
|
|
231
|
+
|
|
232
|
+
if (this.cachingFieldData && !this.cachingFieldData.some((data) => data.key === item.key)) {
|
|
233
|
+
this.cachingFieldData.push(item);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function CreateHTML_controller() {
|
|
239
|
+
return dom.utils.createElement('DIV', { class: 'se-controller se-empty-controller' }, '<div></div>');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default Mention;
|