suneditor 3.2.2 → 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.
- 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 +21 -0
- package/src/core/event/effects/keydown.registry.js +77 -0
- package/src/core/event/eventOrchestrator.js +13 -0
- package/src/core/event/handlers/handler_ww_input.js +12 -5
- package/src/core/event/handlers/handler_ww_key.js +5 -3
- package/src/core/event/ports.js +12 -2
- package/src/core/event/reducers/keydown.reducer.js +19 -9
- package/src/core/event/rules/keydown.rule.backspace.js +14 -0
- package/src/core/event/rules/keydown.rule.delete.js +29 -0
- package/src/core/event/rules/keydown.rule.enter.js +7 -0
- 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 +3 -2
- 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 +3 -0
- package/types/core/event/effects/keydown.registry.d.ts +6 -0
- package/types/core/event/eventOrchestrator.d.ts +7 -0
- package/types/core/event/handlers/handler_ww_key.d.ts +1 -0
- package/types/core/event/ports.d.ts +4 -2
- 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
|
|
@@ -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 {
|
|
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._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,
|
|
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` (
|
|
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 (!
|
|
58
|
+
if (!useEnterFromBeforeInput(this.$.store, e) && keyCodeMap.isEnter(keyCode)) {
|
|
57
59
|
const normalized = this._normalizeEnterRange();
|
|
58
60
|
if (normalized) selectionNode = normalized;
|
|
59
61
|
}
|
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 {
|
|
@@ -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
|
-
*
|
|
190
|
-
*
|
|
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
|
|
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 (
|
|
@@ -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');
|
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
|
|
|
@@ -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 (
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dom, env } from '../../../helper';
|
|
1
|
+
import { dom, env, numbers } from '../../../helper';
|
|
2
2
|
import { resolveBlock } from './blockResolver';
|
|
3
3
|
import { ResolveButton } from '../../section/constructor';
|
|
4
4
|
import CommandMenu from '../../../modules/ui/CommandMenu.js';
|
|
@@ -14,13 +14,13 @@ const { _w, _d } = env;
|
|
|
14
14
|
*/
|
|
15
15
|
function _resolveLengthPx(value, contextStyle) {
|
|
16
16
|
const trimmed = (value || '').trim();
|
|
17
|
-
const num =
|
|
17
|
+
const num = numbers.get(trimmed, -1);
|
|
18
18
|
if (!num) return 0;
|
|
19
19
|
if (/rem$/.test(trimmed)) {
|
|
20
|
-
return num * (
|
|
20
|
+
return num * (numbers.get(_w.getComputedStyle(_d.documentElement).fontSize, -1) || 16);
|
|
21
21
|
}
|
|
22
22
|
if (/em$/.test(trimmed)) {
|
|
23
|
-
return num * (
|
|
23
|
+
return num * (numbers.get(contextStyle.fontSize, -1) || 16);
|
|
24
24
|
}
|
|
25
25
|
return num;
|
|
26
26
|
}
|
|
@@ -455,9 +455,18 @@ class BlockHandle {
|
|
|
455
455
|
const scrollX = _w.scrollX;
|
|
456
456
|
const scrollY = _w.scrollY;
|
|
457
457
|
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
const
|
|
458
|
+
// First appearance after being hidden — skip transition. Make the handle
|
|
459
|
+
// measurable (display) before reading its height so we can center it.
|
|
460
|
+
const wasHidden = this.#handle.style.display !== 'flex';
|
|
461
|
+
if (wasHidden) {
|
|
462
|
+
dom.utils.addClass(this.#handle, 'se-no-transition');
|
|
463
|
+
this.#handle.style.display = 'flex';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const handleHeight = this.#handle.offsetHeight || 0;
|
|
467
|
+
const firstLineCenter = this.#getFirstLineCenter(blockElement, blockRect);
|
|
468
|
+
const firstLineCenterVP = isIframe ? firstLineCenter + iframeRect.top : firstLineCenter;
|
|
469
|
+
const top = firstLineCenterVP + scrollY - handleHeight / 2;
|
|
461
470
|
|
|
462
471
|
// Handle inline offset
|
|
463
472
|
const isRtl = !!this.#$.options.get('_rtl');
|
|
@@ -469,7 +478,7 @@ class BlockHandle {
|
|
|
469
478
|
const wysiwygFrame = this.#$.frameContext.get('wysiwyg');
|
|
470
479
|
if (wysiwygFrame) {
|
|
471
480
|
const cs = _w.getComputedStyle(wysiwygFrame);
|
|
472
|
-
const padStart =
|
|
481
|
+
const padStart = numbers.get(isRtl ? cs.paddingRight : cs.paddingLeft, -1) || 0;
|
|
473
482
|
const baseline = _resolveLengthPx(cs.getPropertyValue('--se-edit-inner-padding'), cs);
|
|
474
483
|
centeringExtra = Math.max(0, padStart - baseline);
|
|
475
484
|
}
|
|
@@ -477,10 +486,6 @@ class BlockHandle {
|
|
|
477
486
|
|
|
478
487
|
const totalOffset = indent + centeringExtra;
|
|
479
488
|
|
|
480
|
-
// First appearance after being hidden — skip transition
|
|
481
|
-
const wasHidden = this.#handle.style.display !== 'flex';
|
|
482
|
-
if (wasHidden) dom.utils.addClass(this.#handle, 'se-no-transition');
|
|
483
|
-
|
|
484
489
|
this.#handle.style.top = top + 'px';
|
|
485
490
|
if (isRtl) {
|
|
486
491
|
this.#handle.style.left = '';
|
|
@@ -497,6 +502,37 @@ class BlockHandle {
|
|
|
497
502
|
}
|
|
498
503
|
}
|
|
499
504
|
|
|
505
|
+
/**
|
|
506
|
+
* @description Viewport-Y center of the block's first line box (in the block's own document coordinates).
|
|
507
|
+
* @param {HTMLElement} blockElement
|
|
508
|
+
* @param {DOMRect} blockRect - `blockElement.getBoundingClientRect()`
|
|
509
|
+
* @returns {number} Viewport-Y coordinate of the first line's vertical center
|
|
510
|
+
*/
|
|
511
|
+
#getFirstLineCenter(blockElement, blockRect) {
|
|
512
|
+
try {
|
|
513
|
+
const doc = blockElement.ownerDocument;
|
|
514
|
+
const walker = doc.createTreeWalker(blockElement, NodeFilter.SHOW_TEXT, null);
|
|
515
|
+
|
|
516
|
+
let textNode = /** @type {Text|null} */ (null);
|
|
517
|
+
while ((textNode = /** @type {Text} */ (walker.nextNode()))) {
|
|
518
|
+
if (textNode.textContent?.trim()) break;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (textNode) {
|
|
522
|
+
const range = doc.createRange();
|
|
523
|
+
range.setStart(textNode, 0);
|
|
524
|
+
range.setEnd(textNode, Math.min(1, textNode.length));
|
|
525
|
+
const rects = range.getClientRects();
|
|
526
|
+
if (rects.length) return (rects[0].top + rects[0].bottom) / 2;
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
// Fall through to the line-height estimate below
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const lineHeight = numbers.get(_w.getComputedStyle(blockElement).lineHeight, -1) || blockRect.height;
|
|
533
|
+
return blockRect.top + Math.min(lineHeight, blockRect.height) / 2;
|
|
534
|
+
}
|
|
535
|
+
|
|
500
536
|
/**
|
|
501
537
|
* @description Calculate the handle's inline indent.
|
|
502
538
|
* @param {HTMLElement} blockElement
|
|
@@ -512,13 +548,13 @@ class BlockHandle {
|
|
|
512
548
|
const paddingKey = isRtl ? 'paddingRight' : 'paddingLeft';
|
|
513
549
|
let indent = 0;
|
|
514
550
|
|
|
515
|
-
indent +=
|
|
551
|
+
indent += numbers.get(_w.getComputedStyle(blockElement)[marginKey], -1) || 0;
|
|
516
552
|
|
|
517
553
|
let el = blockElement.parentElement;
|
|
518
554
|
while (el && el !== wysiwyg) {
|
|
519
555
|
if (format.isBlock(el)) {
|
|
520
556
|
const s = _w.getComputedStyle(el);
|
|
521
|
-
indent += (
|
|
557
|
+
indent += (numbers.get(s[paddingKey], -1) || 0) + (numbers.get(s[marginKey], -1) || 0);
|
|
522
558
|
}
|
|
523
559
|
el = el.parentElement;
|
|
524
560
|
}
|
|
@@ -888,7 +888,8 @@ class UIManager {
|
|
|
888
888
|
const prev = wysiwyg.querySelector('.se-placeholder-line');
|
|
889
889
|
|
|
890
890
|
const line = this.#store.get('hasFocus') ? this.#$.format.getLine(this.#$.selection.selectionNode) : null;
|
|
891
|
-
const
|
|
891
|
+
const inTableCell = !!line && !!dom.query.getParentElement(line, dom.check.isTableCell);
|
|
892
|
+
const target = dom.check.isEmptyLine(line) && !dom.check.isListCell(line) && !inTableCell ? line : null;
|
|
892
893
|
|
|
893
894
|
// Single-marker invariant: drop the previous marker unless it is still the target line.
|
|
894
895
|
if (prev && prev !== target) {
|
|
@@ -1047,7 +1048,7 @@ class UIManager {
|
|
|
1047
1048
|
|
|
1048
1049
|
function CreateAlertHTML({ lang, icons }) {
|
|
1049
1050
|
const html =
|
|
1050
|
-
'<div><button class="close" data-command="close" title="' +
|
|
1051
|
+
'<div><button type="button" class="close" data-command="close" title="' +
|
|
1051
1052
|
lang.close +
|
|
1052
1053
|
'">' +
|
|
1053
1054
|
icons.cancel +
|