suneditor 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suneditor",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "Vanilla JavaScript based WYSIWYG web editor",
5
5
  "author": "Yi JiHong",
6
6
  "license": "MIT",
@@ -1044,9 +1044,10 @@
1044
1044
  color: var(--se-block-handle-color);
1045
1045
  z-index: 10;
1046
1046
  transition:
1047
- top 0.15s ease-out,
1048
- left 0.15s ease-out,
1049
- right 0.15s ease-out;
1047
+ top 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
1048
+ left 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
1049
+ right 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
1050
+ will-change: top, left, right;
1050
1051
  }
1051
1052
 
1052
1053
  .sun-editor .se-block-handle.se-no-transition {
@@ -112,9 +112,6 @@ class EventOrchestrator extends KernelInjector {
112
112
  this.__eventDoc = null;
113
113
  /** @type {string} */
114
114
  this.__secopy = null;
115
-
116
- /** @type {HTMLInputElement} */
117
- this.__focusTemp = this.#contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
118
115
  }
119
116
 
120
117
  /**
@@ -206,6 +203,32 @@ class EventOrchestrator extends KernelInjector {
206
203
  return this.defaultLineManager.execute(formatName);
207
204
  }
208
205
 
206
+ /**
207
+ * @internal
208
+ * @description Normalize the Enter range before the reducer reads it: reset to a text node, and when the
209
+ * caret sits inside a zero-width text node adjacent to a `<br>`, move it onto the `<br>`.
210
+ * @returns {?(HTMLElement|Text)} The updated selection node, or `null` when the caret is not on a line (no normalization ran).
211
+ */
212
+ _normalizeEnterRange() {
213
+ if (!this.$.format.isLine(this.$.selection.getRange()?.startContainer)) return null;
214
+
215
+ this.$.selection.resetRangeToTextNode();
216
+
217
+ const r = this.$.selection.getRange();
218
+ if (
219
+ r.startContainer === r.endContainer &&
220
+ r.startOffset !== r.endOffset &&
221
+ r.startContainer.nodeType === 3 &&
222
+ dom.check.isZeroWidth(r.startContainer)
223
+ ) {
224
+ const br = r.startContainer.nextSibling;
225
+ if (br && dom.check.isBreak(br)) this.$.selection.setRange(br, 0, br, 0);
226
+ else this.$.selection.setRange(r.endContainer, r.endOffset, r.endContainer, r.endOffset);
227
+ }
228
+
229
+ return this.$.selection.getNode();
230
+ }
231
+
209
232
  /**
210
233
  * @internal
211
234
  * @description Handles data transfer actions for `paste` and `drop` events.
@@ -579,9 +602,6 @@ class EventOrchestrator extends KernelInjector {
579
602
  this.#eventManager.addEvent(parent, 'scroll', OnScrollAbs, false);
580
603
  }
581
604
 
582
- /** focus temp (mobile) */
583
- this.#eventManager.addEvent(this.__focusTemp, 'focus', (e) => e.preventDefault(), false);
584
-
585
605
  /** document event */
586
606
  if (this.__eventDoc !== fc.get('_wd')) {
587
607
  this.__eventDoc = fc.get('_wd');
@@ -658,7 +678,6 @@ class EventOrchestrator extends KernelInjector {
658
678
  this.__inputPlugin = null;
659
679
  this.__inputBlurEvent = null;
660
680
  this.__inputKeyEvent = null;
661
- this.__focusTemp = null;
662
681
  this.__eventDoc = null;
663
682
  this.__secopy = null;
664
683
  this._lineBreakComp = null;
@@ -1,4 +1,12 @@
1
1
  import { dom, keyCodeMap } from '../../../helper';
2
+ import { actionExecutor } from '../executor';
3
+ import { makePorts } from '../ports';
4
+ import { reduceEnterDown } from '../rules/keydown.rule.enter';
5
+ import { ENTER_FROM_BEFOREINPUT } from '../reducers/keydown.reducer';
6
+
7
+ // The Enter rule/effects never touch the retain-style node cache (a backspace concern) — a local
8
+ // placeholder is enough to satisfy `makePorts`.
9
+ const _enterStyleNodes = { value: [] };
2
10
 
3
11
  /**
4
12
  * @typedef {import('../eventOrchestrator').default} EventManagerThis_handler_ww_input
@@ -16,6 +24,14 @@ export async function OnBeforeInput_wysiwyg(fc, e) {
16
24
  return false;
17
25
  }
18
26
 
27
+ // Enter is dispatched here — not on keydown — so the IME has finished committing before the DOM
28
+ // mutates (iOS/mobile marked-text stability). `insertParagraph` = Enter, `insertLineBreak` = Shift+Enter.
29
+ // ctrl/alt+Enter are shortcuts and never produce these inputTypes, so they stay on the keydown path.
30
+ if (ENTER_FROM_BEFOREINPUT && (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak')) {
31
+ await dispatchEnter.call(this, fc, e);
32
+ return;
33
+ }
34
+
19
35
  const data = (e.data === null ? '' : e.data === undefined ? ' ' : e.data) || '';
20
36
  if (!keyCodeMap.isComposing(e)) {
21
37
  if (!this.$.char.test(data, false)) {
@@ -35,6 +51,65 @@ export async function OnBeforeInput_wysiwyg(fc, e) {
35
51
  await this._callPluginEventAsync('onBeforeInput', { frameContext: fc, event: e, data });
36
52
  }
37
53
 
54
+ /**
55
+ * @this {EventManagerThis_handler_ww_input}
56
+ * @description Runs SunEditor's Enter logic from the `beforeinput` event (post-IME-commit) by reusing the
57
+ * exact keydown Enter rule + effects — only the dispatch site moves off keydown. See ENTER_FROM_BEFOREINPUT.
58
+ * The guards mirror `OnKeyDown_wysiwyg` (selectMenu, input-element, open-dropdown) so a `beforeinput` Enter
59
+ * is dropped in the same situations a keydown Enter is.
60
+ * @param {SunEditor.FrameContext} fc - Frame context object
61
+ * @param {InputEvent} e - The `beforeinput` event (`insertParagraph` | `insertLineBreak`)
62
+ */
63
+ async function dispatchEnter(fc, e) {
64
+ // Skip while an IME composition is still active
65
+ if (e.isComposing || this.isComposing) return;
66
+ if (this.$.ui.selectMenuOn) return;
67
+
68
+ let selectionNode = this.$.selection.getNode();
69
+ if (dom.check.isInputElement(selectionNode)) return;
70
+ if (this.$.menu.currentDropdownName) return;
71
+
72
+ if (dom.check.isWysiwygFrame(selectionNode)) {
73
+ this._setDefaultLine(this.$.options.get('defaultLine'));
74
+ selectionNode = this.$.selection.getNode();
75
+ }
76
+
77
+ const normalized = this._normalizeEnterRange();
78
+ if (normalized) selectionNode = normalized;
79
+
80
+ const range = this.$.selection.getRange();
81
+ const formatEl = /** @type {HTMLElement} */ (this.$.format.getLine(selectionNode, null) || selectionNode);
82
+ const shift = e.inputType === 'insertLineBreak';
83
+
84
+ /** @type {import('../reducers/keydown.reducer').KeydownReducerCtx} */
85
+ const ctx = {
86
+ e,
87
+ fc,
88
+ store: this.$.store,
89
+ options: this.$.options,
90
+ frameOptions: this.$.frameOptions,
91
+ range,
92
+ selectionNode,
93
+ formatEl,
94
+ keyCode: 'Enter',
95
+ ctrl: false,
96
+ alt: false,
97
+ shift,
98
+ };
99
+
100
+ const ports = makePorts(this, { _styleNodes: _enterStyleNodes });
101
+ const actions = [];
102
+ reduceEnterDown(actions, ports, ctx);
103
+
104
+ // `beforeinput.preventDefault()` MUST run synchronously (before the first `await` below), or the browser
105
+ // commits its native insertParagraph/insertLineBreak on top of ours (duplicate line / cloned container).
106
+ if (actions.some((a) => a.t === 'event.prevent' || a.t === 'event.prevent.stop')) {
107
+ e.preventDefault();
108
+ }
109
+
110
+ await actionExecutor(actions, { ports, ctx });
111
+ }
112
+
38
113
  /**
39
114
  * @this {EventManagerThis_handler_ww_input}
40
115
  * @param {SunEditor.FrameContext} fc - Frame context object
@@ -1,7 +1,7 @@
1
1
  import { dom, env, unicode, keyCodeMap } from '../../../helper';
2
2
  import { actionExecutor } from '../executor';
3
3
  import { makePorts } from '../ports';
4
- import { reduceKeydown } from '../reducers/keydown.reducer';
4
+ import { reduceKeydown, ENTER_FROM_BEFOREINPUT } from '../reducers/keydown.reducer';
5
5
 
6
6
  const { _w } = env;
7
7
  const FRONT_ZEROWIDTH = new RegExp(unicode.zeroWidthSpace + '+', '');
@@ -47,23 +47,15 @@ export async function OnKeyDown_wysiwyg(fc, e) {
47
47
  if (!this.$.store.mode.isSubBalloonAlways) this._hideToolbar_sub();
48
48
  }
49
49
 
50
- /** default key action — normalize the Enter range before the reducer reads it */
51
- if (keyCodeMap.isEnter(keyCode) && this.$.format.isLine(this.$.selection.getRange()?.startContainer)) {
52
- this.$.selection.resetRangeToTextNode();
53
-
54
- const r = this.$.selection.getRange();
55
- if (
56
- r.startContainer === r.endContainer &&
57
- r.startOffset !== r.endOffset &&
58
- r.startContainer.nodeType === 3 &&
59
- dom.check.isZeroWidth(r.startContainer)
60
- ) {
61
- const br = r.startContainer.nextSibling;
62
- if (br && dom.check.isBreak(br)) this.$.selection.setRange(br, 0, br, 0);
63
- else this.$.selection.setRange(r.endContainer, r.endOffset, r.endContainer, r.endOffset);
64
- }
65
-
66
- selectionNode = this.$.selection.getNode();
50
+ /**
51
+ * default key action — normalize the Enter range before the reducer reads it.
52
+ * Skipped when Enter is handled from `beforeinput` (ENTER_FROM_BEFOREINPUT): mutating the DOM here
53
+ * on keydown re-traps the iOS/mobile IME marked-text — `dispatchEnter` runs this same normalization
54
+ * later, after the IME has committed.
55
+ */
56
+ if (!ENTER_FROM_BEFOREINPUT && keyCodeMap.isEnter(keyCode)) {
57
+ const normalized = this._normalizeEnterRange();
58
+ if (normalized) selectionNode = normalized;
67
59
  }
68
60
 
69
61
  const range = this.$.selection.getRange();
@@ -1,5 +1,3 @@
1
- import { isMobile } from '../../helper/env';
2
-
3
1
  /**
4
2
  * @typedef {import('./eventOrchestrator').default} EventManagerInstanceType
5
3
  */
@@ -187,15 +185,13 @@ export function makePorts(inst, { _styleNodes }) {
187
185
  });
188
186
  },
189
187
  /**
190
- * @description Prevents the default behavior of the `Enter` key and refocuses the editor.
191
- * @param {Event} e The keyboard event
188
+ * @description Prevents the default behavior of the `Enter` key.
189
+ * Enter now runs from `beforeinput` (post-IME-commit), so the former mobile focus-shuffle
190
+ * (temp-focus → refocus, to force-end a virtual-keyboard IME session) is unnecessary.
191
+ * @param {Event} e The keyboard/input event
192
192
  */
193
193
  enterPrevent(e) {
194
194
  e.preventDefault();
195
- if (!isMobile) return;
196
-
197
- inst.__focusTemp.focus({ preventScroll: true });
198
- frameContext.get('wysiwyg').focus({ preventScroll: true });
199
195
  },
200
196
  };
201
197
  }
@@ -9,13 +9,23 @@ import { A } from '../actions';
9
9
 
10
10
  const { isOSX_IOS } = env;
11
11
 
12
+ /**
13
+ * @description Enter is processed from the `beforeinput` event (post-IME-commit) instead of `keydown`,
14
+ * to avoid trapping iOS/mobile IME marked-text when `keydown` mutates the DOM (see `handler_ww_input.js`).
15
+ * Flip to `false` to instantly restore the legacy synchronous `keydown` Enter path — no other file needs
16
+ * touching for rollback (the `keydown` Enter gate, the `handler_ww_key` normalization guard, and the
17
+ * `beforeinput` dispatch all key off this single flag).
18
+ * @type {boolean}
19
+ */
20
+ export const ENTER_FROM_BEFOREINPUT = true;
21
+
12
22
  /**
13
23
  * @typedef {import('../ports').EventReducerPorts} EventPorts
14
24
  */
15
25
 
16
26
  /**
17
27
  * @typedef {Object} KeydownReducerCtx - Keydown Reducer Context object
18
- * @property {KeyboardEvent} ctx.e - The keyboard event
28
+ * @property {KeyboardEvent|InputEvent} ctx.e - The keyboard event (or the `beforeinput` InputEvent when Enter is dispatched from `beforeinput`)
19
29
  * @property {SunEditor.FrameContext} ctx.fc - Frame context object
20
30
  * @property {SunEditor.Store} ctx.store - Editor store object
21
31
  * @property {SunEditor.Options} ctx.options - Options object
@@ -62,8 +72,13 @@ export async function reduceKeydown(ports, ctx) {
62
72
  break;
63
73
  }
64
74
  case 'Enter' /** enter key */: {
65
- if (reduceEnterDown(actions, ports, ctx) === false) {
66
- return actions;
75
+ // Enter is handled in `beforeinput` (IME stability) — see ENTER_FROM_BEFOREINPUT.
76
+ // Gate only (not an early-return) so the post-switch `documentTypeRefreshHeader`
77
+ // still runs for a selectRange + Enter on keydown.
78
+ if (!ENTER_FROM_BEFOREINPUT) {
79
+ if (reduceEnterDown(actions, ports, ctx) === false) {
80
+ return actions;
81
+ }
67
82
  }
68
83
  break;
69
84
  }
@@ -76,19 +76,6 @@ function Constructor(editorTargets, options) {
76
76
  // menuTray
77
77
  const menuTray = dom.utils.createElement('DIV', { class: 'se-menu-tray', popover: 'manual' });
78
78
  editor_carrier_wrapper.appendChild(menuTray);
79
- // focus temp element
80
- const focusTemp = /** @type {HTMLInputElement} */ (
81
- dom.utils.createElement('INPUT', {
82
- type: 'text',
83
- id: editorFormFieldPrefix + '-focus-temp',
84
- class: '__se__focus__temp__',
85
- autocomplete: 'off',
86
- 'aria-hidden': 'true',
87
- style: 'position: fixed !important; top: -10000px !important; left: -10000px !important; display: block !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important;',
88
- })
89
- );
90
- focusTemp.tabIndex = 0;
91
- editor_carrier_wrapper.appendChild(focusTemp);
92
79
 
93
80
  // modal
94
81
  const modal = dom.utils.createElement('DIV', {
@@ -51,8 +51,6 @@ declare class EventOrchestrator extends KernelInjector {
51
51
  __eventDoc: Document;
52
52
  /** @type {string} */
53
53
  __secopy: string;
54
- /** @type {HTMLInputElement} */
55
- __focusTemp: HTMLInputElement;
56
54
  /**
57
55
  * @description Activates the corresponding button with the tags information of the current cursor position,
58
56
  * - such as `bold`, `underline`, etc., and executes the `active` method of the plugins.
@@ -86,6 +84,13 @@ declare class EventOrchestrator extends KernelInjector {
86
84
  * @param {?string} formatName Format tag name (default: `P`)
87
85
  */
88
86
  _setDefaultLine(formatName: string | null): void;
87
+ /**
88
+ * @internal
89
+ * @description Normalize the Enter range before the reducer reads it: reset to a text node, and when the
90
+ * caret sits inside a zero-width text node adjacent to a `<br>`, move it onto the `<br>`.
91
+ * @returns {?(HTMLElement|Text)} The updated selection node, or `null` when the caret is not on a line (no normalization ran).
92
+ */
93
+ _normalizeEnterRange(): (HTMLElement | Text) | null;
89
94
  /**
90
95
  * @internal
91
96
  * @description Handles data transfer actions for `paste` and `drop` events.
@@ -173,8 +173,10 @@ export function makePorts(
173
173
  */
174
174
  enterScrollTo(range: Range): void;
175
175
  /**
176
- * @description Prevents the default behavior of the `Enter` key and refocuses the editor.
177
- * @param {Event} e The keyboard event
176
+ * @description Prevents the default behavior of the `Enter` key.
177
+ * Enter now runs from `beforeinput` (post-IME-commit), so the former mobile focus-shuffle
178
+ * (temp-focus → refocus, to force-end a virtual-keyboard IME session) is unnecessary.
179
+ * @param {Event} e The keyboard/input event
178
180
  */
179
181
  enterPrevent(e: Event): void;
180
182
  };
@@ -4,7 +4,7 @@ import type {} from '../../../typedef';
4
4
  */
5
5
  /**
6
6
  * @typedef {Object} KeydownReducerCtx - Keydown Reducer Context object
7
- * @property {KeyboardEvent} ctx.e - The keyboard event
7
+ * @property {KeyboardEvent|InputEvent} ctx.e - The keyboard event (or the `beforeinput` InputEvent when Enter is dispatched from `beforeinput`)
8
8
  * @property {SunEditor.FrameContext} ctx.fc - Frame context object
9
9
  * @property {SunEditor.Store} ctx.store - Editor store object
10
10
  * @property {SunEditor.Options} ctx.options - Options object
@@ -27,15 +27,24 @@ import type {} from '../../../typedef';
27
27
  * @returns {Promise<EventActions>} Action list
28
28
  */
29
29
  export function reduceKeydown(ports: EventPorts, ctx: KeydownReducerCtx): Promise<EventActions>;
30
+ /**
31
+ * @description Enter is processed from the `beforeinput` event (post-IME-commit) instead of `keydown`,
32
+ * to avoid trapping iOS/mobile IME marked-text when `keydown` mutates the DOM (see `handler_ww_input.js`).
33
+ * Flip to `false` to instantly restore the legacy synchronous `keydown` Enter path — no other file needs
34
+ * touching for rollback (the `keydown` Enter gate, the `handler_ww_key` normalization guard, and the
35
+ * `beforeinput` dispatch all key off this single flag).
36
+ * @type {boolean}
37
+ */
38
+ export const ENTER_FROM_BEFOREINPUT: boolean;
30
39
  export type EventPorts = import('../ports').EventReducerPorts;
31
40
  /**
32
41
  * - Keydown Reducer Context object
33
42
  */
34
43
  export type KeydownReducerCtx = {
35
44
  /**
36
- * - The keyboard event
45
+ * - The keyboard event (or the `beforeinput` InputEvent when Enter is dispatched from `beforeinput`)
37
46
  */
38
- e: KeyboardEvent;
47
+ e: KeyboardEvent | InputEvent;
39
48
  /**
40
49
  * - Frame context object
41
50
  */