suneditor 3.2.3 → 3.2.4

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.4",
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
  */
@@ -334,5 +334,6 @@ export function reduceBackspaceDown(actions, ports, ctx) {
334
334
  return false;
335
335
  }
336
336
 
337
+ actions.push(A.caretScrollTo(range));
337
338
  return true;
338
339
  }
@@ -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;
@@ -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
  };