suneditor 3.2.1 → 3.2.3

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.
Files changed (39) hide show
  1. package/dist/suneditor.min.css +2 -2
  2. package/dist/suneditor.min.js +1 -1
  3. package/package.json +1 -1
  4. package/src/assets/suneditor.css +8 -1
  5. package/src/core/editor.js +2 -0
  6. package/src/core/event/actions/index.js +21 -0
  7. package/src/core/event/effects/keydown.registry.js +77 -0
  8. package/src/core/event/eventOrchestrator.js +13 -0
  9. package/src/core/event/handlers/handler_ww_input.js +12 -5
  10. package/src/core/event/handlers/handler_ww_key.js +5 -3
  11. package/src/core/event/ports.js +12 -2
  12. package/src/core/event/reducers/keydown.reducer.js +19 -9
  13. package/src/core/event/rules/keydown.rule.backspace.js +14 -0
  14. package/src/core/event/rules/keydown.rule.delete.js +29 -0
  15. package/src/core/event/rules/keydown.rule.enter.js +7 -0
  16. package/src/core/kernel/store.js +2 -0
  17. package/src/core/logic/dom/html.js +13 -1
  18. package/src/core/logic/panel/blockHandle.js +94 -29
  19. package/src/core/logic/shell/ui.js +3 -2
  20. package/src/core/schema/options.js +4 -2
  21. package/src/core/section/constructor.js +16 -1
  22. package/src/helper/converter.js +3 -2
  23. package/src/helper/env.js +51 -0
  24. package/src/helper/markdown.js +3 -1
  25. package/src/modules/contract/Browser.js +2 -2
  26. package/src/modules/ui/CommandMenu.js +1 -1
  27. package/src/plugins/dropdown/table/index.js +3 -2
  28. package/src/plugins/field/slashCommand.js +17 -1
  29. package/types/core/event/actions/index.d.ts +3 -0
  30. package/types/core/event/effects/keydown.registry.d.ts +6 -0
  31. package/types/core/event/eventOrchestrator.d.ts +7 -0
  32. package/types/core/event/handlers/handler_ww_key.d.ts +1 -0
  33. package/types/core/event/ports.d.ts +4 -2
  34. package/types/core/event/reducers/keydown.reducer.d.ts +15 -5
  35. package/types/core/kernel/store.d.ts +5 -0
  36. package/types/core/schema/options.d.ts +7 -4
  37. package/types/helper/env.d.ts +10 -0
  38. package/types/helper/index.d.ts +1 -0
  39. package/types/plugins/field/slashCommand.d.ts +34 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suneditor",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
4
4
  "description": "Vanilla JavaScript based WYSIWYG web editor",
5
5
  "author": "Yi JiHong",
6
6
  "license": "MIT",
@@ -1348,6 +1348,7 @@
1348
1348
 
1349
1349
  /** --- popover UA override ---------------------------------------------------- */
1350
1350
  .sun-editor .se-controller[popover],
1351
+ .sun-editor .se-block-handle[popover],
1351
1352
  .sun-editor.sun-editor-carrier-wrapper .se-drag-cursor[popover] {
1352
1353
  inset: unset;
1353
1354
  margin: 0;
@@ -1358,6 +1359,7 @@
1358
1359
  .sun-editor .se-loading-box[popover],
1359
1360
  .sun-editor .se-toast[popover],
1360
1361
  .sun-editor .se-browser[popover],
1362
+ .sun-editor .se-block-handle[popover],
1361
1363
  .sun-editor .se-menu-tray[popover] {
1362
1364
  background: none;
1363
1365
  border: none;
@@ -3327,7 +3329,7 @@
3327
3329
  margin: 0;
3328
3330
  min-height: 28px;
3329
3331
  }
3330
- .sun-editor .se-select-menu .se-select-item .se-btn-list span {
3332
+ .sun-editor .se-select-menu .se-select-item .se-btn-list > span {
3331
3333
  padding: 0;
3332
3334
  margin: 0;
3333
3335
  }
@@ -4376,6 +4378,11 @@
4376
4378
  direction: rtl;
4377
4379
  }
4378
4380
 
4381
+ /* toolbar */
4382
+ .sun-editor.se-rtl .se-btn-tray {
4383
+ direction: rtl;
4384
+ }
4385
+
4379
4386
  /* statusbar */
4380
4387
  .sun-editor.se-rtl .se-status-bar {
4381
4388
  direction: rtl;
@@ -139,6 +139,8 @@ class Editor {
139
139
  this.$.store.set('initViewportHeight', env._w.visualViewport.height);
140
140
  this.#kernel._eventOrchestrator.__setViewportSize();
141
141
 
142
+ this.$.store.set('_canUseBeforeInput', env.canUseBeforeInput());
143
+
142
144
  this.$.contextProvider.init();
143
145
 
144
146
  // initialize core and add event listeners
@@ -149,6 +149,13 @@ export const A = {
149
149
  p: { rowEndBr, rowStartBr },
150
150
  }),
151
151
 
152
+ /**
153
+ * @description Soft line break merge (Backspace): the caret sits in the zero-width anchor right after a soft-break `<br>` (a Shift+Enter).
154
+ * @param {Node} zws - the zero-width Text node holding the caret (its `previousSibling` is the `<br>`)
155
+ * @returns {Action}
156
+ */
157
+ backspaceSoftBreakMerge: (zws) => ({ t: 'backspace.softBreak.merge', p: { zws } }),
158
+
152
159
  // === delete ===
153
160
  /**
154
161
  * @param {Element} formatEl
@@ -187,6 +194,13 @@ export const A = {
187
194
  */
188
195
  deleteBrLineRowMerge: (rowEndBr) => ({ t: 'delete.brline.rowMerge', p: { rowEndBr } }),
189
196
 
197
+ /**
198
+ * @description Soft line break merge (Delete): the caret sits just before a soft-break `<br>`.
199
+ * @param {Node} br - the soft-break `<br>` to remove
200
+ * @returns {Action}
201
+ */
202
+ deleteSoftBreakMerge: (br) => ({ t: 'delete.softBreak.merge', p: { br } }),
203
+
190
204
  // === tab ===
191
205
  /**
192
206
  * @param {Range} range
@@ -253,6 +267,13 @@ export const A = {
253
267
  * @returns {Action}
254
268
  */
255
269
  enterBrLineInsert: (range) => ({ t: 'enter.brline.insert', p: { range } }),
270
+ /**
271
+ * @description Soft line break (Shift+Enter) — insert a `<br>` at the caret, splitting the line.
272
+ * Used instead of the browser's native `insertLineBreak`, which is inconsistent across browsers.
273
+ * @param {Range} range Caret range captured at rule time.
274
+ * @returns {Action}
275
+ */
276
+ enterShiftBr: (range) => ({ t: 'enter.shift.br', p: { range } }),
256
277
  /**
257
278
  * @description Exit a normal brLine — drop its trailing empty rows and add a default line after it.
258
279
  * @param {Element} brBlock
@@ -155,6 +155,19 @@ export default {
155
155
  }
156
156
  },
157
157
 
158
+ /** @action backspaceSoftBreakMerge */
159
+ 'backspace.softBreak.merge': ({ ports }, { zws }) => {
160
+ const br = zws.previousSibling;
161
+ const parent = zws.parentNode;
162
+ const before = br?.previousSibling;
163
+
164
+ dom.utils.removeItem(zws);
165
+ if (br) dom.utils.removeItem(br);
166
+
167
+ if (before) caretToNodeEnd(ports, before);
168
+ else if (parent) ports.selection.setRange(parent, 0, parent, 0);
169
+ },
170
+
158
171
  /** [delete] */
159
172
 
160
173
  /** @action deleteComponentSelect */
@@ -252,6 +265,19 @@ export default {
252
265
  }
253
266
  },
254
267
 
268
+ /** @action deleteSoftBreakMerge */
269
+ 'delete.softBreak.merge': ({ ports }, { br }) => {
270
+ const before = br.previousSibling;
271
+ const parent = br.parentNode;
272
+ const zws = br.nextSibling;
273
+
274
+ if (zws && zws.nodeType === 3 && dom.check.isZeroWidth(zws.textContent)) dom.utils.removeItem(zws);
275
+ dom.utils.removeItem(br);
276
+
277
+ if (before) caretToNodeEnd(ports, before);
278
+ else if (parent) ports.selection.setRange(parent, 0, parent, 0);
279
+ },
280
+
255
281
  /** [tab] */
256
282
 
257
283
  /** @action tabFormatIndent */
@@ -486,6 +512,46 @@ export default {
486
512
  ports.setOnShortcutKey(true);
487
513
  },
488
514
 
515
+ /** @action enterShiftBr — soft line break (Shift+Enter): insert a `<br>` at the caret, splitting the line. */
516
+ 'enter.shift.br': ({ ports, ctx }, { range }) => {
517
+ const wd = ctx.fc.get('_wd');
518
+
519
+ const domSel = ports.selection.get();
520
+ const live = domSel?.rangeCount > 0 && ctx.fc.get('wysiwyg').contains(domSel.focusNode);
521
+ const src = live ? domSel.getRangeAt(0) : range;
522
+
523
+ const r = wd.createRange();
524
+ r.setStart(src.startContainer, src.startOffset);
525
+ r.setEnd(src.endContainer, src.endOffset);
526
+ if (!src.collapsed) {
527
+ r.deleteContents();
528
+ r.collapse(true);
529
+ }
530
+
531
+ if (r.startContainer.nodeType === 1 && dom.check.isBreak(r.startContainer)) {
532
+ const brNode = r.startContainer;
533
+ r.setStart(
534
+ brNode.parentNode,
535
+ dom.utils.getArrayIndex(brNode.parentNode.childNodes, brNode) + r.startOffset,
536
+ );
537
+ r.collapse(true);
538
+ }
539
+
540
+ const br = dom.utils.createElement('BR');
541
+ r.insertNode(br);
542
+
543
+ const next = br.nextSibling;
544
+ if (next && next.nodeType === 3 && !dom.check.isZeroWidth(next.textContent)) {
545
+ ports.selection.setRange(next, 0, next, 0);
546
+ } else {
547
+ const zws = dom.utils.createTextNode(unicode.zeroWidthSpace);
548
+ br.parentNode.insertBefore(zws, br.nextSibling);
549
+ ports.selection.setRange(zws, 1, zws, 1);
550
+ }
551
+
552
+ ports.history.push(false);
553
+ },
554
+
489
555
  /** @action enterBrLineExit — consume only the caret's current (last) empty row and add a default line after the brLine. */
490
556
  'enter.brline.exit': ({ ports }, { brBlock }) => {
491
557
  const last = brBlock.lastChild;
@@ -676,6 +742,17 @@ function caretToLineEdge(ports, line, toEnd) {
676
742
  ports.selection.setRange(line, offset, line, offset);
677
743
  }
678
744
 
745
+ /**
746
+ * @description Place a collapsed caret at the END of `node`, whether it is a Text node (end of its text)
747
+ * or an element (after its last child). A fixed offset would land mid-node for a multi-child inline wrapper.
748
+ * @param {import('../ports').EventReducerPorts} ports
749
+ * @param {Node} node
750
+ */
751
+ function caretToNodeEnd(ports, node) {
752
+ const offset = node.nodeType === 3 ? node.textContent.length : node.childNodes.length;
753
+ ports.selection.setRange(node, offset, node, offset);
754
+ }
755
+
679
756
  /**
680
757
  * @param {HTMLElement} formatEl - Format element
681
758
  * @returns {Node}
@@ -66,6 +66,12 @@ class EventOrchestrator extends KernelInjector {
66
66
  */
67
67
  this.isComposing = false;
68
68
 
69
+ /**
70
+ * @description Real Shift-key state of the most recent Enter keydown.
71
+ * @type {boolean}
72
+ */
73
+ this._enterKeyShift = false;
74
+
69
75
  /**
70
76
  * @description An array of parent containers that can be scrolled (in descending order)
71
77
  * @type {Array<Element>}
@@ -112,6 +118,9 @@ class EventOrchestrator extends KernelInjector {
112
118
  this.__eventDoc = null;
113
119
  /** @type {string} */
114
120
  this.__secopy = null;
121
+
122
+ /** @type {HTMLInputElement} */
123
+ this.__focusTemp = this.#contextProvider.carrierWrapper.querySelector('.__se__focus__temp__');
115
124
  }
116
125
 
117
126
  /**
@@ -602,6 +611,9 @@ class EventOrchestrator extends KernelInjector {
602
611
  this.#eventManager.addEvent(parent, 'scroll', OnScrollAbs, false);
603
612
  }
604
613
 
614
+ /** focus temp (mobile) — used by the legacy `keydown` Enter path to force-end a virtual-keyboard IME session */
615
+ this.#eventManager.addEvent(this.__focusTemp, 'focus', (e) => e.preventDefault(), false);
616
+
605
617
  /** document event */
606
618
  if (this.__eventDoc !== fc.get('_wd')) {
607
619
  this.__eventDoc = fc.get('_wd');
@@ -678,6 +690,7 @@ class EventOrchestrator extends KernelInjector {
678
690
  this.__inputPlugin = null;
679
691
  this.__inputBlurEvent = null;
680
692
  this.__inputKeyEvent = null;
693
+ this.__focusTemp = null;
681
694
  this.__eventDoc = null;
682
695
  this.__secopy = null;
683
696
  this._lineBreakComp = null;
@@ -2,7 +2,7 @@ import { dom, keyCodeMap } from '../../../helper';
2
2
  import { actionExecutor } from '../executor';
3
3
  import { makePorts } from '../ports';
4
4
  import { reduceEnterDown } from '../rules/keydown.rule.enter';
5
- import { ENTER_FROM_BEFOREINPUT } from '../reducers/keydown.reducer';
5
+ import { useEnterFromBeforeInput } from '../reducers/keydown.reducer';
6
6
 
7
7
  // The Enter rule/effects never touch the retain-style node cache (a backspace concern) — a local
8
8
  // placeholder is enough to satisfy `makePorts`.
@@ -27,7 +27,10 @@ export async function OnBeforeInput_wysiwyg(fc, e) {
27
27
  // Enter is dispatched here — not on keydown — so the IME has finished committing before the DOM
28
28
  // mutates (iOS/mobile marked-text stability). `insertParagraph` = Enter, `insertLineBreak` = Shift+Enter.
29
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')) {
30
+ if (
31
+ useEnterFromBeforeInput(this.$.store) &&
32
+ (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak')
33
+ ) {
31
34
  await dispatchEnter.call(this, fc, e);
32
35
  return;
33
36
  }
@@ -74,12 +77,16 @@ async function dispatchEnter(fc, e) {
74
77
  selectionNode = this.$.selection.getNode();
75
78
  }
76
79
 
77
- const normalized = this._normalizeEnterRange();
78
- if (normalized) selectionNode = normalized;
80
+ const shift = this._enterKeyShift || e.inputType === 'insertLineBreak';
81
+ this._enterKeyShift = false;
82
+
83
+ if (!shift) {
84
+ const normalized = this._normalizeEnterRange();
85
+ if (normalized) selectionNode = normalized;
86
+ }
79
87
 
80
88
  const range = this.$.selection.getRange();
81
89
  const formatEl = /** @type {HTMLElement} */ (this.$.format.getLine(selectionNode, null) || selectionNode);
82
- const shift = e.inputType === 'insertLineBreak';
83
90
 
84
91
  /** @type {import('../reducers/keydown.reducer').KeydownReducerCtx} */
85
92
  const ctx = {
@@ -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, ENTER_FROM_BEFOREINPUT } from '../reducers/keydown.reducer';
4
+ import { reduceKeydown, useEnterFromBeforeInput } from '../reducers/keydown.reducer';
5
5
 
6
6
  const { _w } = env;
7
7
  const FRONT_ZEROWIDTH = new RegExp(unicode.zeroWidthSpace + '+', '');
@@ -34,6 +34,8 @@ export async function OnKeyDown_wysiwyg(fc, e) {
34
34
  const ctrl = (keyState.ctrl = keyCodeMap.isCtrl(e));
35
35
  const alt = (keyState.alt = keyCodeMap.isAlt(e));
36
36
 
37
+ if (keyCodeMap.isEnter(keyCode)) this._enterKeyShift = shift;
38
+
37
39
  if (!ctrl && fc.get('isReadOnly') && !keyCodeMap.isDirectionKey(keyCode)) {
38
40
  e.preventDefault();
39
41
  return false;
@@ -49,11 +51,11 @@ export async function OnKeyDown_wysiwyg(fc, e) {
49
51
 
50
52
  /**
51
53
  * 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
54
+ * Skipped when Enter is handled from `beforeinput` (useEnterFromBeforeInput): mutating the DOM here
53
55
  * on keydown re-traps the iOS/mobile IME marked-text — `dispatchEnter` runs this same normalization
54
56
  * later, after the IME has committed.
55
57
  */
56
- if (!ENTER_FROM_BEFOREINPUT && keyCodeMap.isEnter(keyCode)) {
58
+ if (!useEnterFromBeforeInput(this.$.store, e) && keyCodeMap.isEnter(keyCode)) {
57
59
  const normalized = this._normalizeEnterRange();
58
60
  if (normalized) selectionNode = normalized;
59
61
  }
@@ -1,3 +1,6 @@
1
+ import { isMobile } from '../../helper/env';
2
+ import { useEnterFromBeforeInput } from './reducers/keydown.reducer';
3
+
1
4
  /**
2
5
  * @typedef {import('./eventOrchestrator').default} EventManagerInstanceType
3
6
  */
@@ -94,6 +97,7 @@ export function makePorts(inst, { _styleNodes }) {
94
97
  history,
95
98
  char,
96
99
  menu,
100
+ store,
97
101
  } = inst.$;
98
102
 
99
103
  return {
@@ -186,12 +190,18 @@ export function makePorts(inst, { _styleNodes }) {
186
190
  },
187
191
  /**
188
192
  * @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.
193
+ * On the `beforeinput` Enter path the IME has already committed, so a plain `preventDefault` suffices.
194
+ * On the legacy `keydown` path (environment doesn't deliver `beforeinput` — see `useEnterFromBeforeInput`)
195
+ * a mobile virtual keyboard can leave an open IME session, so we force it to end with a focus-shuffle
196
+ * (temp-focus → refocus wysiwyg); otherwise the just-committed marked-text is re-trapped.
191
197
  * @param {Event} e The keyboard/input event
192
198
  */
193
199
  enterPrevent(e) {
194
200
  e.preventDefault();
201
+ if (!isMobile || useEnterFromBeforeInput(store)) return;
202
+
203
+ inst.__focusTemp.focus({ preventScroll: true });
204
+ frameContext.get('wysiwyg').focus({ preventScroll: true });
195
205
  },
196
206
  };
197
207
  }
@@ -10,15 +10,28 @@ import { A } from '../actions';
10
10
  const { isOSX_IOS } = env;
11
11
 
12
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).
13
+ * @description Master switch for processing Enter from the `beforeinput` event (post-IME-commit) instead
14
+ * of `keydown`, to avoid trapping iOS/mobile IME marked-text when `keydown` mutates the DOM (see
15
+ * `handler_ww_input.js`).
16
+ *
17
+ * This is only the compile-time master; the effective per-environment decision is
18
+ * {@link useEnterFromBeforeInput}, which additionally requires that this environment actually delivers
19
+ * `beforeinput` at runtime (some corporate security SW / DLP / VDI drop it).
18
20
  * @type {boolean}
19
21
  */
20
22
  export const ENTER_FROM_BEFOREINPUT = true;
21
23
 
24
+ /**
25
+ * @description Routing decision for Enter. Auto-repeat (held key) fires `keydown` but not `beforeinput`,
26
+ * so a repeat must stay on `keydown` or only the first line break would land.
27
+ * @param {SunEditor.Store} store - Editor store object
28
+ * @param {KeyboardEvent|InputEvent} [e] - Source event; a `repeat` flag forces the `keydown` path.
29
+ * @returns {boolean} `true` to route Enter through `beforeinput`, `false` to keep it on `keydown`.
30
+ */
31
+ export function useEnterFromBeforeInput(store, e) {
32
+ return ENTER_FROM_BEFOREINPUT && store.get('_canUseBeforeInput') && !(/** @type {KeyboardEvent} */ (e)?.repeat);
33
+ }
34
+
22
35
  /**
23
36
  * @typedef {import('../ports').EventReducerPorts} EventPorts
24
37
  */
@@ -72,10 +85,7 @@ export async function reduceKeydown(ports, ctx) {
72
85
  break;
73
86
  }
74
87
  case 'Enter' /** enter key */: {
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) {
88
+ if (!useEnterFromBeforeInput(ctx.store, ctx.e)) {
79
89
  if (reduceEnterDown(actions, ports, ctx) === false) {
80
90
  return actions;
81
91
  }
@@ -304,6 +304,20 @@ export function reduceBackspaceDown(actions, ports, ctx) {
304
304
  }
305
305
  }
306
306
 
307
+ // soft line break (Shift+Enter)
308
+ if (
309
+ !selectRange &&
310
+ range.startContainer.nodeType === 3 &&
311
+ dom.check.isZeroWidth(range.startContainer) &&
312
+ range.startOffset <= 1 &&
313
+ dom.check.isBreak(range.startContainer.previousSibling)
314
+ ) {
315
+ actions.push(A.preventStop());
316
+ actions.push(A.backspaceSoftBreakMerge(range.startContainer));
317
+ actions.push(A.historyPush(true));
318
+ return false;
319
+ }
320
+
307
321
  // empty line: merge into the previous line
308
322
  const emptyLinePrev = formatEl?.previousElementSibling;
309
323
  if (
@@ -169,6 +169,35 @@ export function reduceDeleteDown(actions, ports, ctx) {
169
169
  }
170
170
  }
171
171
 
172
+ // soft line break (Shift+Enter)
173
+ if (!selectRange) {
174
+ const dcon = range.startContainer;
175
+ let softBr = null;
176
+
177
+ if (
178
+ dcon.nodeType === 3 &&
179
+ range.startOffset >= dcon.textContent.length &&
180
+ dom.check.isBreak(dcon.nextSibling)
181
+ ) {
182
+ const br = dcon.nextSibling;
183
+ if (br.nextSibling?.nodeType === 3 && dom.check.isZeroWidth(br.nextSibling.textContent)) softBr = br;
184
+ } else if (
185
+ dcon.nodeType === 3 &&
186
+ dom.check.isZeroWidth(dcon) &&
187
+ range.startOffset === 0 &&
188
+ dom.check.isBreak(dcon.previousSibling)
189
+ ) {
190
+ softBr = dcon.previousSibling;
191
+ }
192
+
193
+ if (softBr) {
194
+ actions.push(A.preventStop());
195
+ actions.push(A.deleteSoftBreakMerge(softBr));
196
+ actions.push(A.historyPush(true));
197
+ return false;
198
+ }
199
+ }
200
+
172
201
  // empty line
173
202
  const emptyLineNext = formatEl?.nextElementSibling;
174
203
  if (
@@ -76,6 +76,13 @@ export function reduceEnterDown(actions, ports, ctx) {
76
76
  }
77
77
  }
78
78
 
79
+ if (shift) {
80
+ ports.enterPrevent(e);
81
+ actions.push(A.enterShiftBr(range));
82
+ actions.push(A.enterScrollTo(range));
83
+ return true;
84
+ }
85
+
79
86
  if (!shift) {
80
87
  let formatEndEdge = format.isEdgeLine(range.endContainer, range.endOffset, 'end');
81
88
  let formatStartEdge = format.isEdgeLine(range.startContainer, range.startOffset, 'front');
@@ -18,6 +18,7 @@ import { numbers } from '../../helper';
18
18
  * @property {boolean} _mousedown - Whether `mousedown` is pressed.
19
19
  * @property {boolean} _preventBlur - Suppress `blur` event handling.
20
20
  * @property {boolean} _preventFocus - Suppress `focus` event handling.
21
+ * @property {boolean} _canUseBeforeInput - Whether this environment actually delivers `beforeinput` at runtime (probed once at editor load; some corporate security SW / DLP / VDI drop it). Defaults `true`.
21
22
  */
22
23
 
23
24
  /**
@@ -100,6 +101,7 @@ class Store {
100
101
  _mousedown: false,
101
102
  _preventBlur: false,
102
103
  _preventFocus: false,
104
+ _canUseBeforeInput: true,
103
105
  };
104
106
  }
105
107
 
@@ -3,6 +3,7 @@ import { dom, converter, markdown, numbers, unicode, clipboard, env } from '../.
3
3
  const { _d } = env;
4
4
  const REQUIRED_DATA_ATTRS = 'data-se-[^\\s]+';
5
5
  const V2_MIG_DATA_ATTRS = '|data-index|data-file-size|data-file-name|data-exp|data-font-size';
6
+ const ZWS_RUN_REGEXP = new RegExp(unicode.zeroWidthSpace + '+', 'g');
6
7
 
7
8
  /**
8
9
  * @description All HTML related classes involved in the editing area
@@ -365,6 +366,14 @@ class HTML {
365
366
  cleanData = this.#styleNodeConvertor(cleanData);
366
367
  }
367
368
 
369
+ if (!_freeCodeViewMode && cleanData.includes(unicode.zeroWidthSpace)) {
370
+ cleanData = cleanData.replace(ZWS_RUN_REGEXP, (m, offset, str) => {
371
+ const boundedStart = offset === 0 || str[offset - 1] === '>';
372
+ const boundedEnd = offset + m.length === str.length || str[offset + m.length] === '<';
373
+ return boundedStart && boundedEnd ? m : '';
374
+ });
375
+ }
376
+
368
377
  return cleanData;
369
378
  }
370
379
 
@@ -2228,7 +2237,9 @@ class HTML {
2228
2237
  v.push(sv[0]);
2229
2238
  }
2230
2239
  } else if (!v || !_RE_STYLE_EQ.test(v.toString())) {
2231
- if (this.#cleanStyleTagKeyRegExp.test(tagName)) {
2240
+ if (_RE_SE_COMPONENT.test(m)) {
2241
+ v = this.#cleanStyle(m, v, '@component');
2242
+ } else if (this.#cleanStyleTagKeyRegExp.test(tagName)) {
2232
2243
  v = this.#cleanStyle(m, v, tagName);
2233
2244
  } else if (this.#$.format.isLine(tagName)) {
2234
2245
  v = this.#cleanStyle(m, v, '@line');
@@ -2356,6 +2367,7 @@ const _RE_TAG_NAME = /(?!<)[a-zA-Z0-9-]+/;
2356
2367
  const _RE_ON_HANDLER = /\s(?:on[a-z]+)\s*=\s*(?:(["'])[^"']*\1|\S+)/gi;
2357
2368
  const _RE_STYLE_EQ = /style=/i;
2358
2369
  const _RE_STYLE_ATTR = /style\s*=\s*(?:"|')[^"']*(?:"|')/;
2370
+ const _RE_SE_COMPONENT = /class\s*=\s*("|')(?:[^"']*\s)?se-component(?=[\s"'])/i;
2359
2371
  const _RE_LEADING_SPACE = /^\s/;
2360
2372
 
2361
2373
  // #cleanStyle