suneditor 3.1.0 → 3.1.2
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 +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +2 -2
- package/src/assets/design/size.css +2 -1
- package/src/assets/suneditor.css +47 -21
- package/src/core/editor.js +1 -0
- package/src/core/event/actions/index.js +2 -1
- package/src/core/event/effects/keydown.registry.js +3 -1
- package/src/core/event/effects/ruleHelpers.js +30 -1
- package/src/core/event/rules/keydown.rule.arrow.js +22 -16
- package/src/core/event/rules/keydown.rule.backspace.js +18 -5
- package/src/core/event/rules/keydown.rule.delete.js +7 -5
- package/src/core/event/rules/keydown.rule.enter.js +33 -3
- package/src/core/logic/dom/format.js +5 -1
- package/src/core/logic/panel/menu.js +74 -3
- package/src/core/logic/shell/ui.js +39 -1
- package/src/core/section/constructor.js +15 -1
- package/src/plugins/field/autocomplete.js +42 -5
- package/types/core/event/actions/index.d.ts +1 -1
- package/types/core/event/effects/keydown.registry.d.ts +1 -1
- package/types/core/event/effects/ruleHelpers.d.ts +12 -0
- package/types/core/logic/shell/ui.d.ts +2 -2
- package/types/plugins/field/autocomplete.d.ts +84 -10
|
@@ -200,14 +200,27 @@ class UIManager {
|
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
202
|
* @description Set direction to `rtl` or `ltr`.
|
|
203
|
-
* @param {
|
|
203
|
+
* @param {"rtl"|"ltr"} dir `rtl` or `ltr`
|
|
204
204
|
*/
|
|
205
205
|
setDir(dir) {
|
|
206
206
|
const rtl = dir === 'rtl';
|
|
207
207
|
if (this.#options.get('_rtl') === rtl) return;
|
|
208
208
|
|
|
209
|
+
const prevDir = this.#options.get('textDirection');
|
|
210
|
+
const prevEditableClass = this.#options.get('_editableClass');
|
|
211
|
+
const prevPrintClass = this.#options.get('printClass');
|
|
212
|
+
|
|
209
213
|
try {
|
|
210
214
|
this.#options.set('_rtl', rtl);
|
|
215
|
+
this.#options.set('textDirection', dir);
|
|
216
|
+
|
|
217
|
+
// update _editableClass / printClass
|
|
218
|
+
const editableClass = rtl ? this.#options.get('_editableClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('_editableClass').replace(/\s*se-rtl/, '');
|
|
219
|
+
this.#options.set('_editableClass', editableClass);
|
|
220
|
+
if (this.#options.get('printClass')) {
|
|
221
|
+
this.#options.set('printClass', rtl ? this.#options.get('printClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('printClass').replace(/\s*se-rtl/, ''));
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
this.offCurrentController();
|
|
212
225
|
|
|
213
226
|
const fc = this.#frameContext;
|
|
@@ -221,11 +234,13 @@ class UIManager {
|
|
|
221
234
|
if (rtl) {
|
|
222
235
|
this.#contextProvider.applyToRoots((e) => {
|
|
223
236
|
dom.utils.addClass([e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')], 'se-rtl');
|
|
237
|
+
e.get('wysiwyg').dir = 'rtl';
|
|
224
238
|
});
|
|
225
239
|
dom.utils.addClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
|
|
226
240
|
} else {
|
|
227
241
|
this.#contextProvider.applyToRoots((e) => {
|
|
228
242
|
dom.utils.removeClass([e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')], 'se-rtl');
|
|
243
|
+
e.get('wysiwyg').removeAttribute('dir');
|
|
229
244
|
});
|
|
230
245
|
dom.utils.removeClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
|
|
231
246
|
}
|
|
@@ -255,6 +270,12 @@ class UIManager {
|
|
|
255
270
|
|
|
256
271
|
this.#activeDirBtn(rtl);
|
|
257
272
|
|
|
273
|
+
// reverse toolbar buttons
|
|
274
|
+
this.#reverseToolbarButtons(this.#context.get('toolbar_buttonTray'));
|
|
275
|
+
if (this.#context.has('toolbar_sub_buttonTray')) {
|
|
276
|
+
this.#reverseToolbarButtons(this.#context.get('toolbar_sub_buttonTray'));
|
|
277
|
+
}
|
|
278
|
+
|
|
258
279
|
// document type
|
|
259
280
|
if (fc.has('documentType_use_header')) {
|
|
260
281
|
if (rtl) fc.get('wrapper').appendChild(fc.get('documentTypeInner'));
|
|
@@ -269,6 +290,9 @@ class UIManager {
|
|
|
269
290
|
else if (this.#store.mode.isSubBalloon) this.#$.subToolbar._showBalloon();
|
|
270
291
|
} catch (e) {
|
|
271
292
|
this.#options.set('_rtl', !rtl);
|
|
293
|
+
this.#options.set('textDirection', prevDir);
|
|
294
|
+
this.#options.set('_editableClass', prevEditableClass);
|
|
295
|
+
if (prevPrintClass !== null) this.#options.set('printClass', prevPrintClass);
|
|
272
296
|
console.warn(`[SUNEDITOR.ui.setDir.fail] ${e.toString()}`);
|
|
273
297
|
}
|
|
274
298
|
|
|
@@ -702,6 +726,20 @@ class UIManager {
|
|
|
702
726
|
}
|
|
703
727
|
}
|
|
704
728
|
|
|
729
|
+
/**
|
|
730
|
+
* @description Reverse the order of toolbar button groups (excluding the more-layer).
|
|
731
|
+
* @param {HTMLElement} buttonTray - The `.se-btn-tray` element.
|
|
732
|
+
*/
|
|
733
|
+
#reverseToolbarButtons(buttonTray) {
|
|
734
|
+
if (!buttonTray) return;
|
|
735
|
+
const moreLayer = buttonTray.querySelector('.se-toolbar-more-layer');
|
|
736
|
+
const children = Array.from(buttonTray.children).filter((c) => c !== moreLayer);
|
|
737
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
738
|
+
buttonTray.appendChild(children[i]);
|
|
739
|
+
}
|
|
740
|
+
if (moreLayer) buttonTray.appendChild(moreLayer);
|
|
741
|
+
}
|
|
742
|
+
|
|
705
743
|
/**
|
|
706
744
|
* @internal
|
|
707
745
|
* @description Set the disabled button list
|
|
@@ -361,8 +361,21 @@ export function CreateShortcuts(command, button, values, keyMap, rc, reverseKeys
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* @description Append tooltip span
|
|
366
|
+
* @param {Element} tooptipBtn
|
|
367
|
+
* @param {boolean} shift
|
|
368
|
+
* @param {string} shortcut
|
|
369
|
+
*/
|
|
364
370
|
function _addTooltip(tooptipBtn, shift, shortcut) {
|
|
365
|
-
|
|
371
|
+
const tooltip = dom.utils.createElement('SPAN', { class: 'se-shortcut' }, env.cmdIcon + (shift ? env.shiftIcon : '') + '+<span class="se-shortcut-key">' + shortcut + '</span>');
|
|
372
|
+
const prevTooltip = tooptipBtn.querySelector('.se-shortcut');
|
|
373
|
+
|
|
374
|
+
if (prevTooltip) {
|
|
375
|
+
tooptipBtn.replaceChild(tooltip, prevTooltip);
|
|
376
|
+
} else {
|
|
377
|
+
tooptipBtn.appendChild(tooltip);
|
|
378
|
+
}
|
|
366
379
|
}
|
|
367
380
|
|
|
368
381
|
/**
|
|
@@ -937,6 +950,7 @@ function _initTargetElements(key, options, topDiv, targetOptions) {
|
|
|
937
950
|
|
|
938
951
|
if (!targetOptions.get('iframe')) {
|
|
939
952
|
wysiwygDiv.setAttribute('contenteditable', 'true');
|
|
953
|
+
if (options.get('_rtl')) wysiwygDiv.dir = 'rtl';
|
|
940
954
|
wysiwygDiv.className += ' ' + options.get('_editableClass');
|
|
941
955
|
wysiwygDiv.style.cssText = editorStyles.frame + editorStyles.editor;
|
|
942
956
|
} else {
|
|
@@ -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
|
|
|
@@ -31,7 +31,7 @@ export namespace A {
|
|
|
31
31
|
function enterFormatCleanBrAndZWS(selectionNode: Node, selectionFormat: boolean, brBlock: Element, children: NodeList, offset: number): Action;
|
|
32
32
|
function enterFormatInsertBrHtml(brBlock: Element, range: Range, wSelection: Selection, offset: number): Action;
|
|
33
33
|
function enterFormatInsertBrNode(wSelection: Selection): Action;
|
|
34
|
-
function enterFormatBreakAtEdge(formatEl: Element, selectionNode: Node, formatStartEdge: boolean, formatEndEdge: boolean): Action;
|
|
34
|
+
function enterFormatBreakAtEdge(formatEl: Element, selectionNode: Node, formatStartEdge: boolean, formatEndEdge: boolean, bidiSwapped?: boolean): Action;
|
|
35
35
|
function enterFormatBreakWithSelection(formatEl: Element, range: Range, formatStartEdge: boolean, formatEndEdge: boolean): Action;
|
|
36
36
|
function enterFormatBreakAtCursor(formatEl: Element, range: Range): Action;
|
|
37
37
|
function enterFigcaptionExitInList(formatEl: Element): Action;
|
|
@@ -42,7 +42,7 @@ declare const _default: {
|
|
|
42
42
|
/** @action enterFormatInsertBrNode */
|
|
43
43
|
'enter.format.insertBrNode': ({ ports }: EffectContext_keydown, { wSelection }: any) => void;
|
|
44
44
|
/** @action enterFormatBreakAtEdge */
|
|
45
|
-
'enter.format.breakAtEdge': ({ ports, ctx }: EffectContext_keydown, { formatEl, selectionNode, formatStartEdge, formatEndEdge }: any) => void;
|
|
45
|
+
'enter.format.breakAtEdge': ({ ports, ctx }: EffectContext_keydown, { formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped }: any) => void;
|
|
46
46
|
/** @action enterFormatBreakWithSelection */
|
|
47
47
|
'enter.format.breakWithSelection': ({ ports, ctx }: EffectContext_keydown, { formatEl, range, formatStartEdge, formatEndEdge }: any) => void;
|
|
48
48
|
/** @action enterFormatBreakAtCursor */
|
|
@@ -34,3 +34,15 @@ export function isUneditableNode(ports: EventPorts, range: Range, isFront: boole
|
|
|
34
34
|
* @returns {void}
|
|
35
35
|
*/
|
|
36
36
|
export function setDefaultLine(ports: EventPorts, lineTagName: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* @description Detects if a detected logical edge is incorrect due to bidi text direction mismatch in RTL mode.
|
|
39
|
+
* When LTR text (numbers, Latin) is inside an RTL line, the browser may place the caret at offset 0
|
|
40
|
+
* for the visual end or offset=length for the visual start. This function compares the caret's visual
|
|
41
|
+
* position against the content boundaries to detect such mismatches.
|
|
42
|
+
* @param {Range} range - The current collapsed range
|
|
43
|
+
* @param {HTMLElement} formatEl - The format/line element
|
|
44
|
+
* @param {'front'|'end'} detectedEdge - The edge detected by logical offset check
|
|
45
|
+
* @param {Document} doc - The document object
|
|
46
|
+
* @returns {boolean} true if the detected edge doesn't match the visual position (bidi mismatch)
|
|
47
|
+
*/
|
|
48
|
+
export function isRtlBidiMismatch(range: Range, formatEl: HTMLElement, detectedEdge: 'front' | 'end', doc: Document): boolean;
|
|
@@ -51,9 +51,9 @@ declare class UIManager {
|
|
|
51
51
|
setTheme(theme: string): void;
|
|
52
52
|
/**
|
|
53
53
|
* @description Set direction to `rtl` or `ltr`.
|
|
54
|
-
* @param {
|
|
54
|
+
* @param {"rtl"|"ltr"} dir `rtl` or `ltr`
|
|
55
55
|
*/
|
|
56
|
-
setDir(dir:
|
|
56
|
+
setDir(dir: 'rtl' | 'ltr'): void;
|
|
57
57
|
/**
|
|
58
58
|
* @description Switch to or off `ReadOnly` mode.
|
|
59
59
|
* @param {boolean} value `readOnly` boolean value.
|
|
@@ -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
|
/**
|