suneditor 3.2.2 → 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/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +1 -1
- package/src/assets/suneditor.css +6 -1
- package/src/core/editor.js +2 -0
- package/src/core/event/actions/index.js +26 -2
- package/src/core/event/effects/keydown.registry.js +83 -4
- package/src/core/event/eventOrchestrator.js +20 -3
- package/src/core/event/handlers/handler_ww_input.js +12 -5
- package/src/core/event/handlers/handler_ww_key.js +10 -7
- package/src/core/event/ports.js +16 -8
- package/src/core/event/reducers/keydown.reducer.js +19 -9
- package/src/core/event/rules/keydown.rule.backspace.js +15 -0
- package/src/core/event/rules/keydown.rule.delete.js +29 -0
- package/src/core/event/rules/keydown.rule.enter.js +15 -8
- package/src/core/kernel/store.js +2 -0
- package/src/core/logic/dom/html.js +13 -1
- package/src/core/logic/panel/blockHandle.js +50 -14
- package/src/core/logic/shell/ui.js +7 -5
- package/src/core/schema/options.js +4 -2
- package/src/core/section/constructor.js +15 -0
- package/src/helper/converter.js +3 -2
- package/src/helper/env.js +51 -0
- package/src/helper/markdown.js +3 -1
- package/src/modules/contract/Browser.js +2 -2
- package/src/modules/ui/CommandMenu.js +1 -1
- package/src/plugins/dropdown/table/index.js +3 -2
- package/src/plugins/field/slashCommand.js +17 -1
- package/types/core/event/actions/index.d.ts +4 -1
- package/types/core/event/effects/keydown.registry.d.ts +9 -2
- package/types/core/event/eventOrchestrator.d.ts +14 -3
- package/types/core/event/handlers/handler_ww_key.d.ts +1 -0
- package/types/core/event/ports.d.ts +8 -6
- package/types/core/event/reducers/keydown.reducer.d.ts +15 -5
- package/types/core/kernel/store.d.ts +5 -0
- package/types/core/schema/options.d.ts +7 -4
- package/types/helper/env.d.ts +10 -0
- package/types/helper/index.d.ts +1 -0
- package/types/plugins/field/slashCommand.d.ts +34 -2
package/package.json
CHANGED
package/src/assets/suneditor.css
CHANGED
|
@@ -3329,7 +3329,7 @@
|
|
|
3329
3329
|
margin: 0;
|
|
3330
3330
|
min-height: 28px;
|
|
3331
3331
|
}
|
|
3332
|
-
.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 {
|
|
3333
3333
|
padding: 0;
|
|
3334
3334
|
margin: 0;
|
|
3335
3335
|
}
|
|
@@ -4378,6 +4378,11 @@
|
|
|
4378
4378
|
direction: rtl;
|
|
4379
4379
|
}
|
|
4380
4380
|
|
|
4381
|
+
/* toolbar */
|
|
4382
|
+
.sun-editor.se-rtl .se-btn-tray {
|
|
4383
|
+
direction: rtl;
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4381
4386
|
/* statusbar */
|
|
4382
4387
|
.sun-editor.se-rtl .se-status-bar {
|
|
4383
4388
|
direction: rtl;
|
package/src/core/editor.js
CHANGED
|
@@ -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
|
|
@@ -196,12 +210,15 @@ export const A = {
|
|
|
196
210
|
*/
|
|
197
211
|
tabFormatIndent: (range, formatEl, shift) => ({ t: 'tab.format.indent', p: { range, formatEl, shift } }),
|
|
198
212
|
|
|
199
|
-
// ===
|
|
213
|
+
// === caret ===
|
|
200
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.
|
|
201
216
|
* @param {Range} range Range object
|
|
202
217
|
* @returns {Action}
|
|
203
218
|
*/
|
|
204
|
-
|
|
219
|
+
caretScrollTo: (range) => ({ t: 'caret.scrollTo', p: { range } }),
|
|
220
|
+
|
|
221
|
+
// === enter ===
|
|
205
222
|
/**
|
|
206
223
|
* @param {Element} formatEl
|
|
207
224
|
* @returns {Action}
|
|
@@ -253,6 +270,13 @@ export const A = {
|
|
|
253
270
|
* @returns {Action}
|
|
254
271
|
*/
|
|
255
272
|
enterBrLineInsert: (range) => ({ t: 'enter.brline.insert', p: { range } }),
|
|
273
|
+
/**
|
|
274
|
+
* @description Soft line break (Shift+Enter) — insert a `<br>` at the caret, splitting the line.
|
|
275
|
+
* Used instead of the browser's native `insertLineBreak`, which is inconsistent across browsers.
|
|
276
|
+
* @param {Range} range Caret range captured at rule time.
|
|
277
|
+
* @returns {Action}
|
|
278
|
+
*/
|
|
279
|
+
enterShiftBr: (range) => ({ t: 'enter.shift.br', p: { range } }),
|
|
256
280
|
/**
|
|
257
281
|
* @description Exit a normal brLine — drop its trailing empty rows and add a default line after it.
|
|
258
282
|
* @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 */
|
|
@@ -376,13 +402,15 @@ export default {
|
|
|
376
402
|
ports.selection.setRange(r.sc, r.so, r.ec, r.eo);
|
|
377
403
|
},
|
|
378
404
|
|
|
379
|
-
/** [
|
|
405
|
+
/** [caret] */
|
|
380
406
|
|
|
381
|
-
/** @action
|
|
382
|
-
'
|
|
383
|
-
ports.
|
|
407
|
+
/** @action caretScrollTo */
|
|
408
|
+
'caret.scrollTo': ({ ports }, { range }) => {
|
|
409
|
+
ports.caretScrollTo(range);
|
|
384
410
|
},
|
|
385
411
|
|
|
412
|
+
/** [enter] */
|
|
413
|
+
|
|
386
414
|
/** @action enterLineAddDefault */
|
|
387
415
|
'enter.line.addDefault': ({ ports }, { formatEl }) => {
|
|
388
416
|
const newFormat = ports.format.addLineAfter(formatEl);
|
|
@@ -486,6 +514,46 @@ export default {
|
|
|
486
514
|
ports.setOnShortcutKey(true);
|
|
487
515
|
},
|
|
488
516
|
|
|
517
|
+
/** @action enterShiftBr — soft line break (Shift+Enter): insert a `<br>` at the caret, splitting the line. */
|
|
518
|
+
'enter.shift.br': ({ ports, ctx }, { range }) => {
|
|
519
|
+
const wd = ctx.fc.get('_wd');
|
|
520
|
+
|
|
521
|
+
const domSel = ports.selection.get();
|
|
522
|
+
const live = domSel?.rangeCount > 0 && ctx.fc.get('wysiwyg').contains(domSel.focusNode);
|
|
523
|
+
const src = live ? domSel.getRangeAt(0) : range;
|
|
524
|
+
|
|
525
|
+
const r = wd.createRange();
|
|
526
|
+
r.setStart(src.startContainer, src.startOffset);
|
|
527
|
+
r.setEnd(src.endContainer, src.endOffset);
|
|
528
|
+
if (!src.collapsed) {
|
|
529
|
+
r.deleteContents();
|
|
530
|
+
r.collapse(true);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (r.startContainer.nodeType === 1 && dom.check.isBreak(r.startContainer)) {
|
|
534
|
+
const brNode = r.startContainer;
|
|
535
|
+
r.setStart(
|
|
536
|
+
brNode.parentNode,
|
|
537
|
+
dom.utils.getArrayIndex(brNode.parentNode.childNodes, brNode) + r.startOffset,
|
|
538
|
+
);
|
|
539
|
+
r.collapse(true);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const br = dom.utils.createElement('BR');
|
|
543
|
+
r.insertNode(br);
|
|
544
|
+
|
|
545
|
+
const next = br.nextSibling;
|
|
546
|
+
if (next && next.nodeType === 3 && !dom.check.isZeroWidth(next.textContent)) {
|
|
547
|
+
ports.selection.setRange(next, 0, next, 0);
|
|
548
|
+
} else {
|
|
549
|
+
const zws = dom.utils.createTextNode(unicode.zeroWidthSpace);
|
|
550
|
+
br.parentNode.insertBefore(zws, br.nextSibling);
|
|
551
|
+
ports.selection.setRange(zws, 1, zws, 1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
ports.history.push(false);
|
|
555
|
+
},
|
|
556
|
+
|
|
489
557
|
/** @action enterBrLineExit — consume only the caret's current (last) empty row and add a default line after the brLine. */
|
|
490
558
|
'enter.brline.exit': ({ ports }, { brBlock }) => {
|
|
491
559
|
const last = brBlock.lastChild;
|
|
@@ -676,6 +744,17 @@ function caretToLineEdge(ports, line, toEnd) {
|
|
|
676
744
|
ports.selection.setRange(line, offset, line, offset);
|
|
677
745
|
}
|
|
678
746
|
|
|
747
|
+
/**
|
|
748
|
+
* @description Place a collapsed caret at the END of `node`, whether it is a Text node (end of its text)
|
|
749
|
+
* or an element (after its last child). A fixed offset would land mid-node for a multi-child inline wrapper.
|
|
750
|
+
* @param {import('../ports').EventReducerPorts} ports
|
|
751
|
+
* @param {Node} node
|
|
752
|
+
*/
|
|
753
|
+
function caretToNodeEnd(ports, node) {
|
|
754
|
+
const offset = node.nodeType === 3 ? node.textContent.length : node.childNodes.length;
|
|
755
|
+
ports.selection.setRange(node, offset, node, offset);
|
|
756
|
+
}
|
|
757
|
+
|
|
679
758
|
/**
|
|
680
759
|
* @param {HTMLElement} formatEl - Format element
|
|
681
760
|
* @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
|
/**
|
|
@@ -205,11 +214,15 @@ class EventOrchestrator extends KernelInjector {
|
|
|
205
214
|
|
|
206
215
|
/**
|
|
207
216
|
* @internal
|
|
208
|
-
* @description Normalize the
|
|
209
|
-
*
|
|
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.
|
|
210
223
|
* @returns {?(HTMLElement|Text)} The updated selection node, or `null` when the caret is not on a line (no normalization ran).
|
|
211
224
|
*/
|
|
212
|
-
|
|
225
|
+
_normalizeEditRange() {
|
|
213
226
|
if (!this.$.format.isLine(this.$.selection.getRange()?.startContainer)) return null;
|
|
214
227
|
|
|
215
228
|
this.$.selection.resetRangeToTextNode();
|
|
@@ -602,6 +615,9 @@ class EventOrchestrator extends KernelInjector {
|
|
|
602
615
|
this.#eventManager.addEvent(parent, 'scroll', OnScrollAbs, false);
|
|
603
616
|
}
|
|
604
617
|
|
|
618
|
+
/** focus temp (mobile) — used by the legacy `keydown` Enter path to force-end a virtual-keyboard IME session */
|
|
619
|
+
this.#eventManager.addEvent(this.__focusTemp, 'focus', (e) => e.preventDefault(), false);
|
|
620
|
+
|
|
605
621
|
/** document event */
|
|
606
622
|
if (this.__eventDoc !== fc.get('_wd')) {
|
|
607
623
|
this.__eventDoc = fc.get('_wd');
|
|
@@ -678,6 +694,7 @@ class EventOrchestrator extends KernelInjector {
|
|
|
678
694
|
this.__inputPlugin = null;
|
|
679
695
|
this.__inputBlurEvent = null;
|
|
680
696
|
this.__inputKeyEvent = null;
|
|
697
|
+
this.__focusTemp = null;
|
|
681
698
|
this.__eventDoc = null;
|
|
682
699
|
this.__secopy = null;
|
|
683
700
|
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 {
|
|
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 (
|
|
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
|
|
78
|
-
|
|
80
|
+
const shift = this._enterKeyShift || e.inputType === 'insertLineBreak';
|
|
81
|
+
this._enterKeyShift = false;
|
|
82
|
+
|
|
83
|
+
if (!shift) {
|
|
84
|
+
const normalized = this._normalizeEditRange();
|
|
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,
|
|
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;
|
|
@@ -48,13 +50,14 @@ export async function OnKeyDown_wysiwyg(fc, e) {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
51
|
-
* default key action — normalize the
|
|
52
|
-
*
|
|
53
|
-
* on keydown re-traps the iOS/mobile IME marked-text
|
|
54
|
-
*
|
|
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).
|
|
55
57
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const enterOnKeydown = keyCodeMap.isEnter(keyCode) && !useEnterFromBeforeInput(this.$.store, e);
|
|
59
|
+
if (enterOnKeydown || keyCodeMap.isRemoveKey(keyCode)) {
|
|
60
|
+
const normalized = this._normalizeEditRange();
|
|
58
61
|
if (normalized) selectionNode = normalized;
|
|
59
62
|
}
|
|
60
63
|
|
package/src/core/event/ports.js
CHANGED
|
@@ -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 {
|
|
@@ -169,15 +173,13 @@ export function makePorts(inst, { _styleNodes }) {
|
|
|
169
173
|
formatAttrsTempCache: (attrs) => (inst._formatAttrsTemp = attrs),
|
|
170
174
|
setOnShortcutKey: (v) => (inst._onShortcutKey = v),
|
|
171
175
|
|
|
172
|
-
// === enter event specific ===
|
|
173
176
|
/**
|
|
174
|
-
* @description Scrolls the editor view to the caret position after
|
|
175
|
-
* @param {Range} range Pre-
|
|
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).
|
|
176
179
|
*/
|
|
177
|
-
|
|
180
|
+
caretScrollTo(range) {
|
|
178
181
|
ui._iframeAutoHeight(frameContext);
|
|
179
182
|
|
|
180
|
-
// Scroll to the *live* post-Enter caret, not the pre-Enter `range` snapshot.
|
|
181
183
|
selection.scrollTo(selection.getRange() || range, {
|
|
182
184
|
behavior: 'auto',
|
|
183
185
|
block: 'nearest',
|
|
@@ -186,12 +188,18 @@ export function makePorts(inst, { _styleNodes }) {
|
|
|
186
188
|
},
|
|
187
189
|
/**
|
|
188
190
|
* @description Prevents the default behavior of the `Enter` key.
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
+
* On the `beforeinput` Enter path the IME has already committed, so a plain `preventDefault` suffices.
|
|
192
|
+
* On the legacy `keydown` path (environment doesn't deliver `beforeinput` — see `useEnterFromBeforeInput`)
|
|
193
|
+
* a mobile virtual keyboard can leave an open IME session, so we force it to end with a focus-shuffle
|
|
194
|
+
* (temp-focus → refocus wysiwyg); otherwise the just-committed marked-text is re-trapped.
|
|
191
195
|
* @param {Event} e The keyboard/input event
|
|
192
196
|
*/
|
|
193
197
|
enterPrevent(e) {
|
|
194
198
|
e.preventDefault();
|
|
199
|
+
if (!isMobile || useEnterFromBeforeInput(store)) return;
|
|
200
|
+
|
|
201
|
+
inst.__focusTemp.focus({ preventScroll: true });
|
|
202
|
+
frameContext.get('wysiwyg').focus({ preventScroll: true });
|
|
195
203
|
},
|
|
196
204
|
};
|
|
197
205
|
}
|
|
@@ -221,5 +229,5 @@ export function makePorts(inst, { _styleNodes }) {
|
|
|
221
229
|
* @property {(v: boolean) => void} setOnShortcutKey
|
|
222
230
|
*
|
|
223
231
|
* @property {(e: Event) => void} enterPrevent
|
|
224
|
-
* @property {(range: Range) => void}
|
|
232
|
+
* @property {(range: Range) => void} caretScrollTo
|
|
225
233
|
*/
|
|
@@ -10,15 +10,28 @@ import { A } from '../actions';
|
|
|
10
10
|
const { isOSX_IOS } = env;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @description
|
|
14
|
-
* to avoid trapping iOS/mobile IME marked-text when `keydown` mutates the DOM (see
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
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 (
|
|
@@ -320,5 +334,6 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
320
334
|
return false;
|
|
321
335
|
}
|
|
322
336
|
|
|
337
|
+
actions.push(A.caretScrollTo(range));
|
|
323
338
|
return true;
|
|
324
339
|
}
|
|
@@ -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.caretScrollTo(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');
|
|
@@ -100,7 +107,7 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
100
107
|
if (formatEndEdge && (/^H[1-6]$/i.test(formatEl.nodeName) || /^HR$/i.test(formatEl.nodeName))) {
|
|
101
108
|
ports.enterPrevent(e);
|
|
102
109
|
actions.push(A.enterLineAddDefault(formatEl));
|
|
103
|
-
actions.push(A.
|
|
110
|
+
actions.push(A.caretScrollTo(range));
|
|
104
111
|
return true;
|
|
105
112
|
} else if (rangeEl && formatEl && !dom.check.isTableCell(rangeEl) && !/^FIGCAPTION$/i.test(rangeEl.nodeName)) {
|
|
106
113
|
// add default List line
|
|
@@ -111,7 +118,7 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
111
118
|
) {
|
|
112
119
|
ports.enterPrevent(e);
|
|
113
120
|
actions.push(A.enterListAddItem(formatEl, selectionNode));
|
|
114
|
-
actions.push(A.
|
|
121
|
+
actions.push(A.caretScrollTo(range));
|
|
115
122
|
return true;
|
|
116
123
|
}
|
|
117
124
|
|
|
@@ -139,7 +146,7 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
139
146
|
} else {
|
|
140
147
|
actions.push(A.enterBrLineInsert(range));
|
|
141
148
|
}
|
|
142
|
-
actions.push(A.
|
|
149
|
+
actions.push(A.caretScrollTo(range));
|
|
143
150
|
return true;
|
|
144
151
|
}
|
|
145
152
|
|
|
@@ -150,7 +157,7 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
150
157
|
} else {
|
|
151
158
|
actions.push(A.enterFormatInsertBrNode(wSelection));
|
|
152
159
|
}
|
|
153
|
-
actions.push(A.
|
|
160
|
+
actions.push(A.caretScrollTo(range));
|
|
154
161
|
return true;
|
|
155
162
|
}
|
|
156
163
|
|
|
@@ -160,7 +167,7 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
160
167
|
actions.push(
|
|
161
168
|
A.enterFormatBreakAtEdge(formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped),
|
|
162
169
|
);
|
|
163
|
-
actions.push(A.
|
|
170
|
+
actions.push(A.caretScrollTo(range));
|
|
164
171
|
return true;
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -187,13 +194,13 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
187
194
|
actions.push(A.enterFormatBreakAtCursor(formatEl, range));
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
actions.push(A.
|
|
197
|
+
actions.push(A.caretScrollTo(range));
|
|
191
198
|
return true;
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
if (selectRange) {
|
|
196
|
-
actions.push(A.
|
|
203
|
+
actions.push(A.caretScrollTo(range));
|
|
197
204
|
return true;
|
|
198
205
|
}
|
|
199
206
|
|
|
@@ -204,7 +211,7 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
204
211
|
) {
|
|
205
212
|
ports.enterPrevent(e);
|
|
206
213
|
actions.push(A.enterFigcaptionExitInList(formatEl));
|
|
207
|
-
actions.push(A.
|
|
214
|
+
actions.push(A.caretScrollTo(range));
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
return true;
|
package/src/core/kernel/store.js
CHANGED
|
@@ -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
|
|