suneditor 3.1.0 → 3.1.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.
- package/dist/suneditor.min.js +1 -1
- package/package.json +1 -1
- package/src/core/event/rules/keydown.rule.enter.js +2 -2
- package/src/core/logic/dom/format.js +5 -1
- package/src/core/logic/panel/menu.js +74 -3
- package/src/plugins/field/autocomplete.js +42 -5
- package/types/plugins/field/autocomplete.d.ts +84 -10
package/package.json
CHANGED
|
@@ -40,8 +40,8 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (!shift) {
|
|
43
|
-
const formatEndEdge =
|
|
44
|
-
const formatStartEdge =
|
|
43
|
+
const formatEndEdge = format.isEdgeLine(range.endContainer, range.endOffset, 'end');
|
|
44
|
+
const formatStartEdge = format.isEdgeLine(range.startContainer, range.startOffset, 'front');
|
|
45
45
|
|
|
46
46
|
// add default format line
|
|
47
47
|
if (formatEndEdge && (/^H[1-6]$/i.test(formatEl.nodeName) || /^HR$/i.test(formatEl.nodeName))) {
|
|
@@ -766,7 +766,11 @@ class Format {
|
|
|
766
766
|
* @returns {node is HTMLElement}
|
|
767
767
|
*/
|
|
768
768
|
isEdgeLine(node, offset, dir) {
|
|
769
|
-
if (
|
|
769
|
+
if (dir === 'front') {
|
|
770
|
+
if (offset > 0) return false;
|
|
771
|
+
} else {
|
|
772
|
+
if (node?.textContent?.length > offset) return false;
|
|
773
|
+
}
|
|
770
774
|
|
|
771
775
|
let result = false;
|
|
772
776
|
const siblingType = dir === 'front' ? 'previousSibling' : 'nextSibling';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { dom, converter } from '../../../helper';
|
|
1
|
+
import { dom, converter, env } from '../../../helper';
|
|
2
|
+
|
|
3
|
+
const { isMobile, _w } = env;
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @description Dropdown and container menu management class
|
|
@@ -22,6 +24,9 @@ class Menu {
|
|
|
22
24
|
#bindMenu_mouseout = null;
|
|
23
25
|
#menuBtn = null;
|
|
24
26
|
#menuContainer = null;
|
|
27
|
+
#deferredShowTimer = null;
|
|
28
|
+
#viewportListener = null;
|
|
29
|
+
#visualViewport = null;
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* @constructor
|
|
@@ -67,6 +72,7 @@ class Menu {
|
|
|
67
72
|
// eventManager member (viewport)
|
|
68
73
|
this.#menuBtn = null;
|
|
69
74
|
this.#menuContainer = null;
|
|
75
|
+
this.#visualViewport = _w.visualViewport || null;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
/**
|
|
@@ -109,7 +115,12 @@ class Menu {
|
|
|
109
115
|
this.currentDropdownType = btnEl.getAttribute('data-type');
|
|
110
116
|
const menu = (this.currentDropdown = this.targetMap[dropdownName]);
|
|
111
117
|
this.currentDropdownActiveButton = btnEl;
|
|
112
|
-
|
|
118
|
+
|
|
119
|
+
if (isMobile) {
|
|
120
|
+
this.#deferMenuShow(btnEl, menu);
|
|
121
|
+
} else {
|
|
122
|
+
this.#setMenuPosition(btnEl, menu);
|
|
123
|
+
}
|
|
113
124
|
|
|
114
125
|
this.#bindClose_dropdown_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.mousedown, false);
|
|
115
126
|
if (this.#dropdownCommands.includes(dropdownName)) {
|
|
@@ -131,6 +142,7 @@ class Menu {
|
|
|
131
142
|
* @description Closes the currently open dropdown menu.
|
|
132
143
|
*/
|
|
133
144
|
dropdownOff() {
|
|
145
|
+
this.#clearDeferredShow();
|
|
134
146
|
this.#removeGlobalEvent();
|
|
135
147
|
if (IsFree(this.currentDropdownType)) this.currentDropdownPlugin?.off?.();
|
|
136
148
|
|
|
@@ -189,7 +201,13 @@ class Menu {
|
|
|
189
201
|
|
|
190
202
|
this.currentContainerActiveButton = /** @type {HTMLButtonElement} */ (button);
|
|
191
203
|
const containerName = (this.currentContainerName = this.currentContainerActiveButton.getAttribute('data-command'));
|
|
192
|
-
this
|
|
204
|
+
this.currentContainer = this.targetMap[containerName];
|
|
205
|
+
|
|
206
|
+
if (isMobile) {
|
|
207
|
+
this.#deferMenuShow(button, this.currentContainer);
|
|
208
|
+
} else {
|
|
209
|
+
this.#setMenuPosition(button, this.currentContainer);
|
|
210
|
+
}
|
|
193
211
|
|
|
194
212
|
this.#bindClose_cons_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.containerDown, false);
|
|
195
213
|
|
|
@@ -201,6 +219,7 @@ class Menu {
|
|
|
201
219
|
* @description Closes the currently open menu container.
|
|
202
220
|
*/
|
|
203
221
|
containerOff() {
|
|
222
|
+
this.#clearDeferredShow();
|
|
204
223
|
this.#removeGlobalEvent();
|
|
205
224
|
|
|
206
225
|
if (this.currentContainer) {
|
|
@@ -231,6 +250,8 @@ class Menu {
|
|
|
231
250
|
*/
|
|
232
251
|
__restoreMenuPosition() {
|
|
233
252
|
if (!this.#menuBtn || !this.#menuContainer) return;
|
|
253
|
+
// skip if deferred show is pending — it will handle positioning after viewport settles
|
|
254
|
+
if (this.#viewportListener || this.#deferredShowTimer) return;
|
|
234
255
|
this.#setMenuPosition(this.#menuBtn, this.#menuContainer);
|
|
235
256
|
}
|
|
236
257
|
|
|
@@ -253,6 +274,56 @@ class Menu {
|
|
|
253
274
|
this.#menuContainer = menu;
|
|
254
275
|
}
|
|
255
276
|
|
|
277
|
+
/**
|
|
278
|
+
* @description Defer menu display on mobile until viewport settles after keyboard dismiss.
|
|
279
|
+
* @param {Node} element Button element
|
|
280
|
+
* @param {HTMLElement} menu Menu element
|
|
281
|
+
*/
|
|
282
|
+
#deferMenuShow(element, menu) {
|
|
283
|
+
this.#clearDeferredShow();
|
|
284
|
+
|
|
285
|
+
menu.style.display = 'none';
|
|
286
|
+
dom.utils.addClass(element.parentElement.children, 'on');
|
|
287
|
+
this.#menuBtn = element;
|
|
288
|
+
this.#menuContainer = menu;
|
|
289
|
+
|
|
290
|
+
let resolved = false;
|
|
291
|
+
const show = (delay) => {
|
|
292
|
+
if (resolved) return;
|
|
293
|
+
resolved = true;
|
|
294
|
+
this.#clearDeferredShow();
|
|
295
|
+
this.#deferredShowTimer = _w.setTimeout(() => {
|
|
296
|
+
this.#deferredShowTimer = null;
|
|
297
|
+
if (this.#menuBtn === element) {
|
|
298
|
+
this.#setMenuPosition(element, menu);
|
|
299
|
+
}
|
|
300
|
+
}, delay);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (this.#visualViewport) {
|
|
304
|
+
// listen for viewport resize (keyboard dismiss) — small delay to let viewport settle
|
|
305
|
+
this.#viewportListener = () => show(10);
|
|
306
|
+
this.#visualViewport.addEventListener('resize', this.#viewportListener, { once: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// fallback if no viewport change occurs (keyboard already hidden or no visualViewport)
|
|
310
|
+
this.#deferredShowTimer = _w.setTimeout(() => show(0), 50);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @description Clear deferred show timer and viewport listener.
|
|
315
|
+
*/
|
|
316
|
+
#clearDeferredShow() {
|
|
317
|
+
if (this.#deferredShowTimer) {
|
|
318
|
+
_w.clearTimeout(this.#deferredShowTimer);
|
|
319
|
+
this.#deferredShowTimer = null;
|
|
320
|
+
}
|
|
321
|
+
if (this.#viewportListener) {
|
|
322
|
+
this.#visualViewport?.removeEventListener('resize', this.#viewportListener);
|
|
323
|
+
this.#viewportListener = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
256
327
|
/**
|
|
257
328
|
* @description Check if the element is part of a more layer
|
|
258
329
|
* @param {Node} element The element to check
|
|
@@ -59,11 +59,48 @@ function defaultOnSelect(item, triggerText) {
|
|
|
59
59
|
* @property {boolean} [useCachingFieldData=true] - Whether to cache selected items for priority display.
|
|
60
60
|
* @property {Object<string, AutocompleteTriggerConfig>} triggers - Per-trigger configurations keyed by trigger character.
|
|
61
61
|
* ```js
|
|
62
|
-
* //
|
|
63
|
-
* {
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
62
|
+
* // Basic usage with static data — mention trigger
|
|
63
|
+
* const editor = SUNEDITOR.create('#editor', {
|
|
64
|
+
* plugins: [autocomplete],
|
|
65
|
+
* autocomplete: {
|
|
66
|
+
* triggers: {
|
|
67
|
+
* '@': {
|
|
68
|
+
* data: [
|
|
69
|
+
* { key: 'john', name: 'John Doe' },
|
|
70
|
+
* { key: 'jane', name: 'Jane Smith' },
|
|
71
|
+
* ],
|
|
72
|
+
* },
|
|
73
|
+
* },
|
|
74
|
+
* },
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // API-based trigger with custom rendering and selection
|
|
78
|
+
* const editor = SUNEDITOR.create('#editor', {
|
|
79
|
+
* plugins: [autocomplete],
|
|
80
|
+
* autocomplete: {
|
|
81
|
+
* delayTime: 200,
|
|
82
|
+
* limitSize: 10,
|
|
83
|
+
* triggers: {
|
|
84
|
+
* '@': {
|
|
85
|
+
* apiUrl: '/api/users?q={key}&limit={limitSize}',
|
|
86
|
+
* apiHeaders: { Authorization: 'Bearer TOKEN' },
|
|
87
|
+
* transformResponse: (json) => json.data.map((u) => ({ key: u.username, name: u.displayName, id: u.id })),
|
|
88
|
+
* renderItem: (item) => `<div class="user-item"><strong>${item.key}</strong> <span>${item.name}</span></div>`,
|
|
89
|
+
* onSelect: (item, trigger) => ({
|
|
90
|
+
* tag: 'a',
|
|
91
|
+
* attrs: { href: `/users/${item.id}`, 'data-se-autocomplete': trigger + item.key },
|
|
92
|
+
* text: trigger + item.key,
|
|
93
|
+
* }),
|
|
94
|
+
* },
|
|
95
|
+
* '#': {
|
|
96
|
+
* apiUrl: '/api/tags?q={key}',
|
|
97
|
+
* transformResponse: (json) => json.tags,
|
|
98
|
+
* searchStartLength: 2,
|
|
99
|
+
* useCachingData: false,
|
|
100
|
+
* },
|
|
101
|
+
* },
|
|
102
|
+
* },
|
|
103
|
+
* });
|
|
67
104
|
* ```
|
|
68
105
|
*/
|
|
69
106
|
|
|
@@ -102,11 +102,48 @@ export type AutocompletePluginOptions = {
|
|
|
102
102
|
/**
|
|
103
103
|
* - Per-trigger configurations keyed by trigger character.
|
|
104
104
|
* ```js
|
|
105
|
-
* //
|
|
106
|
-
* {
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
105
|
+
* // Basic usage with static data — mention trigger
|
|
106
|
+
* const editor = SUNEDITOR.create('#editor', {
|
|
107
|
+
* plugins: [autocomplete],
|
|
108
|
+
* autocomplete: {
|
|
109
|
+
* triggers: {
|
|
110
|
+
* '@': {
|
|
111
|
+
* data: [
|
|
112
|
+
* { key: 'john', name: 'John Doe' },
|
|
113
|
+
* { key: 'jane', name: 'Jane Smith' },
|
|
114
|
+
* ],
|
|
115
|
+
* },
|
|
116
|
+
* },
|
|
117
|
+
* },
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* // API-based trigger with custom rendering and selection
|
|
121
|
+
* const editor = SUNEDITOR.create('#editor', {
|
|
122
|
+
* plugins: [autocomplete],
|
|
123
|
+
* autocomplete: {
|
|
124
|
+
* delayTime: 200,
|
|
125
|
+
* limitSize: 10,
|
|
126
|
+
* triggers: {
|
|
127
|
+
* '@': {
|
|
128
|
+
* apiUrl: '/api/users?q={key}&limit={limitSize}',
|
|
129
|
+
* apiHeaders: { Authorization: 'Bearer TOKEN' },
|
|
130
|
+
* transformResponse: (json) => json.data.map((u) => ({ key: u.username, name: u.displayName, id: u.id })),
|
|
131
|
+
* renderItem: (item) => `<div class="user-item"><strong>${item.key}</strong> <span>${item.name}</span></div>`,
|
|
132
|
+
* onSelect: (item, trigger) => ({
|
|
133
|
+
* tag: 'a',
|
|
134
|
+
* attrs: { href: `/users/${item.id}`, 'data-se-autocomplete': trigger + item.key },
|
|
135
|
+
* text: trigger + item.key,
|
|
136
|
+
* }),
|
|
137
|
+
* },
|
|
138
|
+
* '#': {
|
|
139
|
+
* apiUrl: '/api/tags?q={key}',
|
|
140
|
+
* transformResponse: (json) => json.tags,
|
|
141
|
+
* searchStartLength: 2,
|
|
142
|
+
* useCachingData: false,
|
|
143
|
+
* },
|
|
144
|
+
* },
|
|
145
|
+
* },
|
|
146
|
+
* });
|
|
110
147
|
* ```
|
|
111
148
|
*/
|
|
112
149
|
triggers: {
|
|
@@ -142,11 +179,48 @@ export type AutocompletePluginOptions = {
|
|
|
142
179
|
* @property {boolean} [useCachingFieldData=true] - Whether to cache selected items for priority display.
|
|
143
180
|
* @property {Object<string, AutocompleteTriggerConfig>} triggers - Per-trigger configurations keyed by trigger character.
|
|
144
181
|
* ```js
|
|
145
|
-
* //
|
|
146
|
-
* {
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
182
|
+
* // Basic usage with static data — mention trigger
|
|
183
|
+
* const editor = SUNEDITOR.create('#editor', {
|
|
184
|
+
* plugins: [autocomplete],
|
|
185
|
+
* autocomplete: {
|
|
186
|
+
* triggers: {
|
|
187
|
+
* '@': {
|
|
188
|
+
* data: [
|
|
189
|
+
* { key: 'john', name: 'John Doe' },
|
|
190
|
+
* { key: 'jane', name: 'Jane Smith' },
|
|
191
|
+
* ],
|
|
192
|
+
* },
|
|
193
|
+
* },
|
|
194
|
+
* },
|
|
195
|
+
* });
|
|
196
|
+
*
|
|
197
|
+
* // API-based trigger with custom rendering and selection
|
|
198
|
+
* const editor = SUNEDITOR.create('#editor', {
|
|
199
|
+
* plugins: [autocomplete],
|
|
200
|
+
* autocomplete: {
|
|
201
|
+
* delayTime: 200,
|
|
202
|
+
* limitSize: 10,
|
|
203
|
+
* triggers: {
|
|
204
|
+
* '@': {
|
|
205
|
+
* apiUrl: '/api/users?q={key}&limit={limitSize}',
|
|
206
|
+
* apiHeaders: { Authorization: 'Bearer TOKEN' },
|
|
207
|
+
* transformResponse: (json) => json.data.map((u) => ({ key: u.username, name: u.displayName, id: u.id })),
|
|
208
|
+
* renderItem: (item) => `<div class="user-item"><strong>${item.key}</strong> <span>${item.name}</span></div>`,
|
|
209
|
+
* onSelect: (item, trigger) => ({
|
|
210
|
+
* tag: 'a',
|
|
211
|
+
* attrs: { href: `/users/${item.id}`, 'data-se-autocomplete': trigger + item.key },
|
|
212
|
+
* text: trigger + item.key,
|
|
213
|
+
* }),
|
|
214
|
+
* },
|
|
215
|
+
* '#': {
|
|
216
|
+
* apiUrl: '/api/tags?q={key}',
|
|
217
|
+
* transformResponse: (json) => json.tags,
|
|
218
|
+
* searchStartLength: 2,
|
|
219
|
+
* useCachingData: false,
|
|
220
|
+
* },
|
|
221
|
+
* },
|
|
222
|
+
* },
|
|
223
|
+
* });
|
|
150
224
|
* ```
|
|
151
225
|
*/
|
|
152
226
|
/**
|