suneditor 3.2.3 → 3.2.5

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.3",
3
+ "version": "3.2.5",
4
4
  "description": "Vanilla JavaScript based WYSIWYG web editor",
5
5
  "author": "Yi JiHong",
6
6
  "license": "MIT",
@@ -210,12 +210,15 @@ export const A = {
210
210
  */
211
211
  tabFormatIndent: (range, formatEl, shift) => ({ t: 'tab.format.indent', p: { range, formatEl, shift } }),
212
212
 
213
- // === enter ===
213
+ // === caret ===
214
214
  /**
215
+ * @description Scroll the editor view to the caret. Used after Enter, Backspace, and any edit that can move the caret out of view.
215
216
  * @param {Range} range Range object
216
217
  * @returns {Action}
217
218
  */
218
- enterScrollTo: (range) => ({ t: 'enter.scrollTo', p: { range } }),
219
+ caretScrollTo: (range) => ({ t: 'caret.scrollTo', p: { range } }),
220
+
221
+ // === enter ===
219
222
  /**
220
223
  * @param {Element} formatEl
221
224
  * @returns {Action}
@@ -402,13 +402,15 @@ export default {
402
402
  ports.selection.setRange(r.sc, r.so, r.ec, r.eo);
403
403
  },
404
404
 
405
- /** [enter] */
405
+ /** [caret] */
406
406
 
407
- /** @action enterScrollTo */
408
- 'enter.scrollTo': ({ ports }, { range }) => {
409
- ports.enterScrollTo(range);
407
+ /** @action caretScrollTo */
408
+ 'caret.scrollTo': ({ ports }, { range }) => {
409
+ ports.caretScrollTo(range);
410
410
  },
411
411
 
412
+ /** [enter] */
413
+
412
414
  /** @action enterLineAddDefault */
413
415
  'enter.line.addDefault': ({ ports }, { formatEl }) => {
414
416
  const newFormat = ports.format.addLineAfter(formatEl);
@@ -214,11 +214,15 @@ class EventOrchestrator extends KernelInjector {
214
214
 
215
215
  /**
216
216
  * @internal
217
- * @description Normalize the Enter range before the reducer reads it: reset to a text node, and when the
218
- * caret sits inside a zero-width text node adjacent to a `<br>`, move it onto the `<br>`.
217
+ * @description Normalize the edit range before the reducer reads it, for every key we custom-handle
218
+ * (Enter, Backspace, Delete). Resets an element-level container (a `line` such as `P`) to a text node so
219
+ * the rules' text-offset arithmetic (`isEdgePoint`, `endOffset === textContent.length`, ...) is valid; and
220
+ * when the caret sits inside a zero-width text node adjacent to a `<br>`, moves it onto the `<br>`.
221
+ * Without this, Backspace/Delete misfire whenever the browser reports the container as the line itself
222
+ * (child-index offset) instead of a `<br>`/ZWS text node.
219
223
  * @returns {?(HTMLElement|Text)} The updated selection node, or `null` when the caret is not on a line (no normalization ran).
220
224
  */
221
- _normalizeEnterRange() {
225
+ _normalizeEditRange() {
222
226
  if (!this.$.format.isLine(this.$.selection.getRange()?.startContainer)) return null;
223
227
 
224
228
  this.$.selection.resetRangeToTextNode();
@@ -81,7 +81,7 @@ async function dispatchEnter(fc, e) {
81
81
  this._enterKeyShift = false;
82
82
 
83
83
  if (!shift) {
84
- const normalized = this._normalizeEnterRange();
84
+ const normalized = this._normalizeEditRange();
85
85
  if (normalized) selectionNode = normalized;
86
86
  }
87
87
 
@@ -50,13 +50,14 @@ export async function OnKeyDown_wysiwyg(fc, e) {
50
50
  }
51
51
 
52
52
  /**
53
- * default key action — normalize the Enter range before the reducer reads it.
54
- * Skipped when Enter is handled from `beforeinput` (useEnterFromBeforeInput): mutating the DOM here
55
- * on keydown re-traps the iOS/mobile IME marked-text — `dispatchEnter` runs this same normalization
56
- * later, after the IME has committed.
53
+ * default key action — normalize the edit range before the reducer reads it, for every custom-handled key.
54
+ * - Enter: skipped when handled from `beforeinput` (useEnterFromBeforeInput),
55
+ * - because mutating the DOM here on keydown re-traps the iOS/mobile IME marked-text.
56
+ * - Backspace/Delete: always normalize on keydown (they have no `beforeinput` route).
57
57
  */
58
- if (!useEnterFromBeforeInput(this.$.store, e) && keyCodeMap.isEnter(keyCode)) {
59
- const normalized = this._normalizeEnterRange();
58
+ const enterOnKeydown = keyCodeMap.isEnter(keyCode) && !useEnterFromBeforeInput(this.$.store, e);
59
+ if (enterOnKeydown || keyCodeMap.isRemoveKey(keyCode)) {
60
+ const normalized = this._normalizeEditRange();
60
61
  if (normalized) selectionNode = normalized;
61
62
  }
62
63
 
@@ -173,15 +173,13 @@ export function makePorts(inst, { _styleNodes }) {
173
173
  formatAttrsTempCache: (attrs) => (inst._formatAttrsTemp = attrs),
174
174
  setOnShortcutKey: (v) => (inst._onShortcutKey = v),
175
175
 
176
- // === enter event specific ===
177
176
  /**
178
- * @description Scrolls the editor view to the caret position after pressing `Enter`.
179
- * @param {Range} range Pre-Enter snapshot range (fallback only).
177
+ * @description Scrolls the editor view to the caret position after an edit (Enter, Backspace, ...).
178
+ * @param {Range} range Pre-edit snapshot range (fallback only).
180
179
  */
181
- enterScrollTo(range) {
180
+ caretScrollTo(range) {
182
181
  ui._iframeAutoHeight(frameContext);
183
182
 
184
- // Scroll to the *live* post-Enter caret, not the pre-Enter `range` snapshot.
185
183
  selection.scrollTo(selection.getRange() || range, {
186
184
  behavior: 'auto',
187
185
  block: 'nearest',
@@ -231,5 +229,5 @@ export function makePorts(inst, { _styleNodes }) {
231
229
  * @property {(v: boolean) => void} setOnShortcutKey
232
230
  *
233
231
  * @property {(e: Event) => void} enterPrevent
234
- * @property {(range: Range) => void} enterScrollTo
232
+ * @property {(range: Range) => void} caretScrollTo
235
233
  */
@@ -40,6 +40,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
40
40
 
41
41
  if (selectRange && hardDelete(ports)) {
42
42
  actions.push(A.preventStop());
43
+ actions.push(A.caretScrollTo(range));
43
44
  return true;
44
45
  }
45
46
 
@@ -66,6 +67,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
66
67
  actions.push(A.preventStop());
67
68
  actions.push(A.delFormatRemoveAndMove(range.startContainer, formatEl));
68
69
  actions.push(A.historyPush(true));
70
+ actions.push(A.caretScrollTo(range));
69
71
  return false;
70
72
  }
71
73
 
@@ -107,6 +109,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
107
109
  }
108
110
 
109
111
  actions.push(A.editorNativeFocus());
112
+ actions.push(A.caretScrollTo(range));
110
113
  return false;
111
114
  }
112
115
 
@@ -201,6 +204,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
201
204
  }
202
205
  }
203
206
 
207
+ actions.push(A.caretScrollTo(range));
204
208
  return true;
205
209
  }
206
210
 
@@ -233,6 +237,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
233
237
  ),
234
238
  );
235
239
  actions.push(A.historyPush(true));
240
+ actions.push(A.caretScrollTo(range));
236
241
  return true;
237
242
  }
238
243
  }
@@ -244,6 +249,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
244
249
  if (fileComponentInfo) {
245
250
  actions.push(A.preventStop());
246
251
  actions.push(A.backspaceComponentRemove(false, formatEl.firstChild, formatEl, fileComponentInfo));
252
+ actions.push(A.caretScrollTo(range));
247
253
  return true;
248
254
  }
249
255
  }
@@ -276,6 +282,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
276
282
  actions.push(A.preventStop());
277
283
  actions.push(A.domUtilsRemoveItem(prev));
278
284
  }
285
+ actions.push(A.caretScrollTo(range));
279
286
  return true;
280
287
  }
281
288
 
@@ -283,6 +290,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
283
290
  if (sel && dom.check.isNonEditable(sel.previousSibling)) {
284
291
  actions.push(A.preventStop());
285
292
  actions.push(A.domUtilsRemoveItem(sel.previousSibling));
293
+ actions.push(A.caretScrollTo(range));
286
294
  return true;
287
295
  }
288
296
  }
@@ -300,6 +308,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
300
308
  actions.push(A.preventStop());
301
309
  actions.push(A.backspaceBrLineRowMerge(range.startContainer, rowStartBr));
302
310
  actions.push(A.historyPush(true));
311
+ actions.push(A.caretScrollTo(range));
303
312
  return false;
304
313
  }
305
314
  }
@@ -315,6 +324,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
315
324
  actions.push(A.preventStop());
316
325
  actions.push(A.backspaceSoftBreakMerge(range.startContainer));
317
326
  actions.push(A.historyPush(true));
327
+ actions.push(A.caretScrollTo(range));
318
328
  return false;
319
329
  }
320
330
 
@@ -331,8 +341,10 @@ export function reduceBackspaceDown(actions, ports, ctx) {
331
341
  actions.push(A.preventStop());
332
342
  actions.push(A.backspaceEmptyLineMergePrev(formatEl, emptyLinePrev));
333
343
  actions.push(A.historyPush(true));
344
+ actions.push(A.caretScrollTo(range));
334
345
  return false;
335
346
  }
336
347
 
348
+ actions.push(A.caretScrollTo(range));
337
349
  return true;
338
350
  }
@@ -79,7 +79,7 @@ export function reduceEnterDown(actions, ports, ctx) {
79
79
  if (shift) {
80
80
  ports.enterPrevent(e);
81
81
  actions.push(A.enterShiftBr(range));
82
- actions.push(A.enterScrollTo(range));
82
+ actions.push(A.caretScrollTo(range));
83
83
  return true;
84
84
  }
85
85
 
@@ -107,7 +107,7 @@ export function reduceEnterDown(actions, ports, ctx) {
107
107
  if (formatEndEdge && (/^H[1-6]$/i.test(formatEl.nodeName) || /^HR$/i.test(formatEl.nodeName))) {
108
108
  ports.enterPrevent(e);
109
109
  actions.push(A.enterLineAddDefault(formatEl));
110
- actions.push(A.enterScrollTo(range));
110
+ actions.push(A.caretScrollTo(range));
111
111
  return true;
112
112
  } else if (rangeEl && formatEl && !dom.check.isTableCell(rangeEl) && !/^FIGCAPTION$/i.test(rangeEl.nodeName)) {
113
113
  // add default List line
@@ -118,7 +118,7 @@ export function reduceEnterDown(actions, ports, ctx) {
118
118
  ) {
119
119
  ports.enterPrevent(e);
120
120
  actions.push(A.enterListAddItem(formatEl, selectionNode));
121
- actions.push(A.enterScrollTo(range));
121
+ actions.push(A.caretScrollTo(range));
122
122
  return true;
123
123
  }
124
124
 
@@ -146,7 +146,7 @@ export function reduceEnterDown(actions, ports, ctx) {
146
146
  } else {
147
147
  actions.push(A.enterBrLineInsert(range));
148
148
  }
149
- actions.push(A.enterScrollTo(range));
149
+ actions.push(A.caretScrollTo(range));
150
150
  return true;
151
151
  }
152
152
 
@@ -157,7 +157,7 @@ export function reduceEnterDown(actions, ports, ctx) {
157
157
  } else {
158
158
  actions.push(A.enterFormatInsertBrNode(wSelection));
159
159
  }
160
- actions.push(A.enterScrollTo(range));
160
+ actions.push(A.caretScrollTo(range));
161
161
  return true;
162
162
  }
163
163
 
@@ -167,7 +167,7 @@ export function reduceEnterDown(actions, ports, ctx) {
167
167
  actions.push(
168
168
  A.enterFormatBreakAtEdge(formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped),
169
169
  );
170
- actions.push(A.enterScrollTo(range));
170
+ actions.push(A.caretScrollTo(range));
171
171
  return true;
172
172
  }
173
173
 
@@ -194,13 +194,13 @@ export function reduceEnterDown(actions, ports, ctx) {
194
194
  actions.push(A.enterFormatBreakAtCursor(formatEl, range));
195
195
  }
196
196
 
197
- actions.push(A.enterScrollTo(range));
197
+ actions.push(A.caretScrollTo(range));
198
198
  return true;
199
199
  }
200
200
  }
201
201
 
202
202
  if (selectRange) {
203
- actions.push(A.enterScrollTo(range));
203
+ actions.push(A.caretScrollTo(range));
204
204
  return true;
205
205
  }
206
206
 
@@ -211,7 +211,7 @@ export function reduceEnterDown(actions, ports, ctx) {
211
211
  ) {
212
212
  ports.enterPrevent(e);
213
213
  actions.push(A.enterFigcaptionExitInList(formatEl));
214
- actions.push(A.enterScrollTo(range));
214
+ actions.push(A.caretScrollTo(range));
215
215
  }
216
216
 
217
217
  return true;
@@ -482,21 +482,28 @@ class Selection_ {
482
482
  el?.scrollIntoView(scrollOption);
483
483
  }
484
484
 
485
- if (scrollOption?.behavior === 'auto' && scrollY !== _w.scrollY) {
486
- if (positionToolbarHeight) {
487
- if (isBottom) {
485
+ if (scrollOption?.behavior === 'auto') {
486
+ if (positionToolbarHeight && !isBottom) {
487
+ // A sticky toolbar overlays the viewport band [0 .. positionToolbarHeight].
488
+ // caret on a wrapped line — and nudge it below the toolbar. Guards:
489
+ // height > 0 — skip a collapsed range's zero rect (phantom full-toolbar scroll)
490
+ // top >= 0 — skip off-screen / mid-animation rects; `behavior:'auto'` follows
491
+ // the page's CSS scroll-behavior, so on `smooth` hosts it's async
492
+ // top < band — only correct when actually under the toolbar; caps the nudge at
493
+ // toolbar height so a stale measurement can't overshoot
494
+ const caretNow = ref?.getBoundingClientRect?.();
495
+ if (caretNow?.height > 0 && caretNow.top >= 0 && caretNow.top < positionToolbarHeight) {
496
+ _w.scrollBy(0, -(positionToolbarHeight - caretNow.top));
497
+ }
498
+ } else if (scrollY !== _w.scrollY) {
499
+ if (positionToolbarHeight) {
488
500
  // bottom toolbar covers the bottom — scroll down more so the element clears the toolbar
489
501
  if (scrollY < _w.scrollY) {
490
502
  _w.scrollBy(0, positionToolbarHeight);
491
503
  }
492
- } else {
493
- // top toolbar covers the top — scroll up more so the element clears the toolbar
494
- if (scrollY > _w.scrollY) {
495
- _w.scrollBy(0, -positionToolbarHeight);
496
- }
504
+ } else if (isAutoHeight) {
505
+ _w.scrollBy(0, statusbarHeight);
497
506
  }
498
- } else if (isAutoHeight) {
499
- _w.scrollBy(0, statusbarHeight);
500
507
  }
501
508
  }
502
509
 
@@ -532,7 +539,11 @@ class Selection_ {
532
539
  const rectTop = refRect?.height > 0 ? refRect.top + scrollY : this.#$.offset.getGlobal(el).top;
533
540
  const scrollMargin = viewHeight + scrollY - rectTop - elH;
534
541
 
535
- if (scrollMargin - PADDING > 0 && viewHeight > scrollMargin + PADDING + topToolbarH) return;
542
+ // Top clearance: with a sticky toolbar, the caret is already clear once its top
543
+ // felt right. With no toolbar (topToolbarH === 0), keep the original PADDING.
544
+ const caretTop = rectTop - scrollY;
545
+ const topClear = topToolbarH > 0 ? caretTop >= topToolbarH : viewHeight > scrollMargin + PADDING;
546
+ if (scrollMargin - PADDING > 0 && topClear) return;
536
547
 
537
548
  const newScrollTop =
538
549
  scrollMargin <= PADDING
@@ -885,14 +885,15 @@ class UIManager {
885
885
  if (!text) return false;
886
886
 
887
887
  const wysiwyg = fc.get('wysiwyg');
888
- const prev = wysiwyg.querySelector('.se-placeholder-line');
889
888
 
890
889
  const line = this.#store.get('hasFocus') ? this.#$.format.getLine(this.#$.selection.selectionNode) : null;
891
890
  const inTableCell = !!line && !!dom.query.getParentElement(line, dom.check.isTableCell);
892
891
  const target = dom.check.isEmptyLine(line) && !dom.check.isListCell(line) && !inTableCell ? line : null;
893
892
 
894
- // Single-marker invariant: drop the previous marker unless it is still the target line.
895
- if (prev && prev !== target) {
893
+ const prevMarkers = wysiwyg.querySelectorAll('.se-placeholder-line');
894
+ for (let i = 0; i < prevMarkers.length; i++) {
895
+ const prev = prevMarkers[i];
896
+ if (prev === target) continue;
896
897
  prev.classList.remove('se-placeholder-line');
897
898
  prev.removeAttribute('data-se-placeholder-line');
898
899
  if (!prev.getAttribute('class')) prev.removeAttribute('class');
@@ -45,7 +45,7 @@ export namespace A {
45
45
  function deleteBrLineRowMerge(rowEndBr: Node): Action;
46
46
  function deleteSoftBreakMerge(br: Node): Action;
47
47
  function tabFormatIndent(range: Range, formatEl: Element, shift: boolean): Action;
48
- function enterScrollTo(range: Range): Action;
48
+ function caretScrollTo(range: Range): Action;
49
49
  function enterLineAddDefault(formatEl: Element): Action;
50
50
  function enterListAddItem(formatEl: Element, selectionNode: Node): Action;
51
51
  function enterFormatExitEmpty(formatEl: Element, rangeEl: Element): Action;
@@ -44,9 +44,10 @@ declare const _default: {
44
44
  /** [tab] */
45
45
  /** @action tabFormatIndent */
46
46
  'tab.format.indent': ({ ports, ctx }: EffectContext_keydown, { range, formatEl, shift }: any) => boolean;
47
+ /** [caret] */
48
+ /** @action caretScrollTo */
49
+ 'caret.scrollTo': ({ ports }: EffectContext_keydown, { range }: any) => void;
47
50
  /** [enter] */
48
- /** @action enterScrollTo */
49
- 'enter.scrollTo': ({ ports }: EffectContext_keydown, { range }: any) => void;
50
51
  /** @action enterLineAddDefault */
51
52
  'enter.line.addDefault': ({ ports }: EffectContext_keydown, { formatEl }: any) => void;
52
53
  /** @action enterListAddItem */
@@ -93,11 +93,15 @@ declare class EventOrchestrator extends KernelInjector {
93
93
  _setDefaultLine(formatName: string | null): void;
94
94
  /**
95
95
  * @internal
96
- * @description Normalize the Enter range before the reducer reads it: reset to a text node, and when the
97
- * caret sits inside a zero-width text node adjacent to a `<br>`, move it onto the `<br>`.
96
+ * @description Normalize the edit range before the reducer reads it, for every key we custom-handle
97
+ * (Enter, Backspace, Delete). Resets an element-level container (a `line` such as `P`) to a text node so
98
+ * the rules' text-offset arithmetic (`isEdgePoint`, `endOffset === textContent.length`, ...) is valid; and
99
+ * when the caret sits inside a zero-width text node adjacent to a `<br>`, moves it onto the `<br>`.
100
+ * Without this, Backspace/Delete misfire whenever the browser reports the container as the line itself
101
+ * (child-index offset) instead of a `<br>`/ZWS text node.
98
102
  * @returns {?(HTMLElement|Text)} The updated selection node, or `null` when the caret is not on a line (no normalization ran).
99
103
  */
100
- _normalizeEnterRange(): (HTMLElement | Text) | null;
104
+ _normalizeEditRange(): (HTMLElement | Text) | null;
101
105
  /**
102
106
  * @internal
103
107
  * @description Handles data transfer actions for `paste` and `drop` events.
@@ -168,10 +168,10 @@ export function makePorts(
168
168
  formatAttrsTempCache: (attrs: any) => any;
169
169
  setOnShortcutKey: (v: any) => any;
170
170
  /**
171
- * @description Scrolls the editor view to the caret position after pressing `Enter`.
172
- * @param {Range} range Pre-Enter snapshot range (fallback only).
171
+ * @description Scrolls the editor view to the caret position after an edit (Enter, Backspace, ...).
172
+ * @param {Range} range Pre-edit snapshot range (fallback only).
173
173
  */
174
- enterScrollTo(range: Range): void;
174
+ caretScrollTo(range: Range): void;
175
175
  /**
176
176
  * @description Prevents the default behavior of the `Enter` key.
177
177
  * On the `beforeinput` Enter path the IME has already committed, so a plain `preventDefault` suffices.
@@ -261,5 +261,5 @@ export type EventReducerPorts = {
261
261
  formatAttrsTempCache: (attrs: { [x: string]: any }) => void;
262
262
  setOnShortcutKey: (v: boolean) => void;
263
263
  enterPrevent: (e: Event) => void;
264
- enterScrollTo: (range: Range) => void;
264
+ caretScrollTo: (range: Range) => void;
265
265
  };