suneditor 3.1.1 → 3.1.2
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-contents.min.css +1 -1
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +2 -2
- package/src/assets/design/size.css +2 -1
- package/src/assets/suneditor.css +47 -21
- package/src/core/editor.js +1 -0
- package/src/core/event/actions/index.js +2 -1
- package/src/core/event/effects/keydown.registry.js +3 -1
- package/src/core/event/effects/ruleHelpers.js +30 -1
- package/src/core/event/rules/keydown.rule.arrow.js +22 -16
- package/src/core/event/rules/keydown.rule.backspace.js +18 -5
- package/src/core/event/rules/keydown.rule.delete.js +7 -5
- package/src/core/event/rules/keydown.rule.enter.js +33 -3
- package/src/core/logic/shell/ui.js +39 -1
- package/src/core/section/constructor.js +15 -1
- package/types/core/event/actions/index.d.ts +1 -1
- package/types/core/event/effects/keydown.registry.d.ts +1 -1
- package/types/core/event/effects/ruleHelpers.d.ts +12 -0
- package/types/core/logic/shell/ui.d.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suneditor",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Vanilla JavaScript based WYSIWYG web editor",
|
|
5
5
|
"author": "Yi JiHong",
|
|
6
6
|
"license": "MIT",
|
|
@@ -168,4 +168,4 @@
|
|
|
168
168
|
"web editor",
|
|
169
169
|
"browser editor"
|
|
170
170
|
]
|
|
171
|
-
}
|
|
171
|
+
}
|
package/src/assets/suneditor.css
CHANGED
|
@@ -963,6 +963,18 @@
|
|
|
963
963
|
bottom: 0px;
|
|
964
964
|
}
|
|
965
965
|
|
|
966
|
+
/* Bottom toolbar: tooltip appears above */
|
|
967
|
+
.sun-editor .se-toolbar.se-toolbar-bottom .se-tooltip .se-tooltip-inner {
|
|
968
|
+
top: auto;
|
|
969
|
+
bottom: 120%;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.sun-editor .se-toolbar.se-toolbar-bottom .se-tooltip .se-tooltip-inner .se-tooltip-text::after {
|
|
973
|
+
top: 100%;
|
|
974
|
+
bottom: auto;
|
|
975
|
+
border-color: var(--se-main-font-color) transparent transparent transparent;
|
|
976
|
+
}
|
|
977
|
+
|
|
966
978
|
/* toolbar arrow up */
|
|
967
979
|
.sun-editor .se-toolbar .se-arrow.se-arrow-up {
|
|
968
980
|
border-bottom-color: var(--se-main-divider-color);
|
|
@@ -1156,7 +1168,7 @@
|
|
|
1156
1168
|
height: auto;
|
|
1157
1169
|
z-index: 5;
|
|
1158
1170
|
border: 1px solid var(--se-main-divider-color);
|
|
1159
|
-
border-radius: var(--se-border-radius);
|
|
1171
|
+
border-radius: var(--se-border-radius-lg);
|
|
1160
1172
|
padding: 4px;
|
|
1161
1173
|
background-color: var(--se-main-background-color);
|
|
1162
1174
|
box-shadow: 0 3px 9px var(--se-shadow-layer-color);
|
|
@@ -1189,7 +1201,8 @@
|
|
|
1189
1201
|
|
|
1190
1202
|
/* dropdown layer - basic list */
|
|
1191
1203
|
.sun-editor .se-list-inner .se-list-basic li {
|
|
1192
|
-
width:
|
|
1204
|
+
width: auto;
|
|
1205
|
+
max-width: 100%;
|
|
1193
1206
|
}
|
|
1194
1207
|
.sun-editor .se-list-inner input {
|
|
1195
1208
|
border-radius: 0;
|
|
@@ -1563,6 +1576,19 @@
|
|
|
1563
1576
|
stroke-width: 2px;
|
|
1564
1577
|
}
|
|
1565
1578
|
|
|
1579
|
+
.sun-editor .se-list-layer .se-selector-color .se-color-pallet:first-child li:first-child button {
|
|
1580
|
+
border-top-left-radius: var(--se-border-radius);
|
|
1581
|
+
}
|
|
1582
|
+
.sun-editor .se-list-layer .se-selector-color .se-color-pallet:first-child li:last-child button {
|
|
1583
|
+
border-top-right-radius: var(--se-border-radius);
|
|
1584
|
+
}
|
|
1585
|
+
.sun-editor .se-list-layer .se-selector-color .se-color-pallet:last-of-type li:first-child button {
|
|
1586
|
+
border-bottom-left-radius: var(--se-border-radius);
|
|
1587
|
+
}
|
|
1588
|
+
.sun-editor .se-list-layer .se-selector-color .se-color-pallet:last-of-type li:last-child button {
|
|
1589
|
+
border-bottom-right-radius: var(--se-border-radius);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1566
1592
|
/* --- hue slider -------------------------------------------------------------- */
|
|
1567
1593
|
.sun-editor .se-hue-slider {
|
|
1568
1594
|
padding: 14px;
|
|
@@ -1997,7 +2023,7 @@
|
|
|
1997
2023
|
-webkit-background-clip: padding-box;
|
|
1998
2024
|
background-clip: padding-box;
|
|
1999
2025
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
2000
|
-
border-radius: var(--se-border-radius);
|
|
2026
|
+
border-radius: var(--se-border-radius-lg);
|
|
2001
2027
|
outline: 0;
|
|
2002
2028
|
box-shadow: 0 3px 9px var(--se-shadow-layer-color);
|
|
2003
2029
|
}
|
|
@@ -2558,6 +2584,7 @@
|
|
|
2558
2584
|
color: var(--se-controller-color);
|
|
2559
2585
|
-webkit-background-clip: padding-box;
|
|
2560
2586
|
background-clip: padding-box;
|
|
2587
|
+
border-radius: var(--se-border-radius);
|
|
2561
2588
|
line-break: auto;
|
|
2562
2589
|
}
|
|
2563
2590
|
|
|
@@ -2582,13 +2609,13 @@
|
|
|
2582
2609
|
|
|
2583
2610
|
.sun-editor-editable[contenteditable='true'] figure figcaption {
|
|
2584
2611
|
display: block;
|
|
2585
|
-
z-index:
|
|
2612
|
+
z-index: 11;
|
|
2586
2613
|
}
|
|
2587
2614
|
|
|
2588
2615
|
.sun-editor-editable[contenteditable='true']:not(.se-read-only) figure:not(.se-input-component)::after {
|
|
2589
2616
|
position: absolute;
|
|
2590
2617
|
content: '';
|
|
2591
|
-
z-index:
|
|
2618
|
+
z-index: 10;
|
|
2592
2619
|
top: 0;
|
|
2593
2620
|
left: 0;
|
|
2594
2621
|
right: 0;
|
|
@@ -2960,10 +2987,10 @@
|
|
|
2960
2987
|
overflow-x: visible;
|
|
2961
2988
|
overflow: visible;
|
|
2962
2989
|
background-color: var(--se-main-background-color);
|
|
2963
|
-
border-radius: var(--se-border-radius);
|
|
2964
|
-
padding:
|
|
2990
|
+
border-radius: var(--se-border-radius-lg);
|
|
2991
|
+
padding: 4px 0;
|
|
2965
2992
|
margin: 0;
|
|
2966
|
-
border: 1px solid var(--se-main-
|
|
2993
|
+
border: 1px solid var(--se-main-divider-color);
|
|
2967
2994
|
box-shadow: 0 3px 9px var(--se-shadow-layer-color);
|
|
2968
2995
|
outline: 0 none;
|
|
2969
2996
|
}
|
|
@@ -2990,8 +3017,10 @@
|
|
|
2990
3017
|
min-height: 28px;
|
|
2991
3018
|
font-size: var(--se-btn-font-size);
|
|
2992
3019
|
padding: 0 6px;
|
|
2993
|
-
margin:
|
|
3020
|
+
margin: 1px 4px;
|
|
2994
3021
|
cursor: pointer;
|
|
3022
|
+
border-radius: var(--se-border-radius);
|
|
3023
|
+
transition: background-color 0.08s ease;
|
|
2995
3024
|
}
|
|
2996
3025
|
|
|
2997
3026
|
.sun-editor .se-select-menu .se-select-item button {
|
|
@@ -3011,21 +3040,12 @@
|
|
|
3011
3040
|
|
|
3012
3041
|
.sun-editor .se-select-menu.se-select-menu-mouse-move .se-select-item:hover,
|
|
3013
3042
|
.sun-editor .se-select-menu:not(.se-select-menu-mouse-move) .se-select-item.active {
|
|
3014
|
-
|
|
3015
|
-
outline: 1px solid var(--se-active-color) !important;
|
|
3016
|
-
box-shadow: 0 0 0 0.3rem var(--se-active-light3-color);
|
|
3017
|
-
transition: box-shadow 0.1s ease-in-out;
|
|
3043
|
+
background-color: var(--se-hover-light-color);
|
|
3018
3044
|
}
|
|
3019
3045
|
|
|
3020
3046
|
.sun-editor .se-select-menu.se-select-menu-mouse-move .se-select-item:active,
|
|
3021
3047
|
.sun-editor .se-select-menu.se-select-menu-mouse-move .se-select-item.__se__active {
|
|
3022
|
-
background-color: var(--se-
|
|
3023
|
-
border-color: var(--se-active-light4-color) !important;
|
|
3024
|
-
outline: 1px solid var(--se-active-color) !important;
|
|
3025
|
-
box-shadow: none;
|
|
3026
|
-
transition:
|
|
3027
|
-
background-color 0.1s ease-in-out,
|
|
3028
|
-
box-shadow 0.1s ease-in-out;
|
|
3048
|
+
background-color: var(--se-hover-light2-color);
|
|
3029
3049
|
}
|
|
3030
3050
|
|
|
3031
3051
|
.sun-editor .se-modal-form-files .se-select-menu {
|
|
@@ -3086,7 +3106,7 @@
|
|
|
3086
3106
|
-webkit-background-clip: padding-box;
|
|
3087
3107
|
background-clip: padding-box;
|
|
3088
3108
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
3089
|
-
border-radius: var(--se-border-radius);
|
|
3109
|
+
border-radius: var(--se-border-radius-lg);
|
|
3090
3110
|
outline: 0;
|
|
3091
3111
|
box-shadow: 0 3px 9px var(--se-shadow-layer-color);
|
|
3092
3112
|
}
|
|
@@ -3943,6 +3963,12 @@
|
|
|
3943
3963
|
.sun-editor.se-rtl .se-find-replace-row {
|
|
3944
3964
|
justify-content: flex-start;
|
|
3945
3965
|
}
|
|
3966
|
+
.sun-editor.se-rtl .se-find-replace-input {
|
|
3967
|
+
direction: rtl;
|
|
3968
|
+
}
|
|
3969
|
+
.sun-editor.se-rtl .se-input-form {
|
|
3970
|
+
direction: rtl;
|
|
3971
|
+
}
|
|
3946
3972
|
|
|
3947
3973
|
/* statusbar */
|
|
3948
3974
|
.sun-editor.se-rtl .se-status-bar {
|
package/src/core/editor.js
CHANGED
|
@@ -245,6 +245,7 @@ class Editor {
|
|
|
245
245
|
converter._setAutoHeightStyle(targetOptions.get('height'));
|
|
246
246
|
frame.contentDocument.body.className = originOptions.get('_editableClass');
|
|
247
247
|
frame.contentDocument.body.setAttribute('contenteditable', 'true');
|
|
248
|
+
if (originOptions.get('_rtl')) frame.contentDocument.body.dir = 'rtl';
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
/**
|
|
@@ -203,9 +203,10 @@ export const A = {
|
|
|
203
203
|
* @param {Node} selectionNode
|
|
204
204
|
* @param {boolean} formatStartEdge
|
|
205
205
|
* @param {boolean} formatEndEdge
|
|
206
|
+
* @param {boolean} [bidiSwapped]
|
|
206
207
|
* @returns {Action}
|
|
207
208
|
*/
|
|
208
|
-
enterFormatBreakAtEdge: (formatEl, selectionNode, formatStartEdge, formatEndEdge) => ({ t: 'enter.format.breakAtEdge', p: { formatEl, selectionNode, formatStartEdge, formatEndEdge } }),
|
|
209
|
+
enterFormatBreakAtEdge: (formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped) => ({ t: 'enter.format.breakAtEdge', p: { formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped } }),
|
|
209
210
|
/**
|
|
210
211
|
* @param {Element} formatEl
|
|
211
212
|
* @param {Range} range
|
|
@@ -411,7 +411,7 @@ export default {
|
|
|
411
411
|
},
|
|
412
412
|
|
|
413
413
|
/** @action enterFormatBreakAtEdge */
|
|
414
|
-
'enter.format.breakAtEdge': ({ ports, ctx }, { formatEl, selectionNode, formatStartEdge, formatEndEdge }) => {
|
|
414
|
+
'enter.format.breakAtEdge': ({ ports, ctx }, { formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped }) => {
|
|
415
415
|
const focusBR = dom.utils.createElement('BR');
|
|
416
416
|
const newFormat = dom.utils.createElement(formatEl.nodeName, null, focusBR);
|
|
417
417
|
|
|
@@ -432,6 +432,8 @@ export default {
|
|
|
432
432
|
formatEl.parentNode.insertBefore(newFormat, formatStartEdge && !formatEndEdge ? formatEl : formatEl.nextElementSibling);
|
|
433
433
|
if (formatEndEdge) {
|
|
434
434
|
ports.selection.setRange(focusBR, 1, focusBR, 1);
|
|
435
|
+
} else if (bidiSwapped) {
|
|
436
|
+
ports.selection.setRange(formatEl, 1, formatEl, 1);
|
|
435
437
|
} else {
|
|
436
438
|
const firstEl = formatEl.firstChild || formatEl;
|
|
437
439
|
ports.selection.setRange(firstEl, 0, firstEl, 0);
|
|
@@ -145,4 +145,33 @@ function setDefaultLine(ports, lineTagName) {
|
|
|
145
145
|
return ports.setDefaultLine(lineTagName);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
/**
|
|
149
|
+
* @description Detects if a detected logical edge is incorrect due to bidi text direction mismatch in RTL mode.
|
|
150
|
+
* When LTR text (numbers, Latin) is inside an RTL line, the browser may place the caret at offset 0
|
|
151
|
+
* for the visual end or offset=length for the visual start. This function compares the caret's visual
|
|
152
|
+
* position against the content boundaries to detect such mismatches.
|
|
153
|
+
* @param {Range} range - The current collapsed range
|
|
154
|
+
* @param {HTMLElement} formatEl - The format/line element
|
|
155
|
+
* @param {'front'|'end'} detectedEdge - The edge detected by logical offset check
|
|
156
|
+
* @param {Document} doc - The document object
|
|
157
|
+
* @returns {boolean} true if the detected edge doesn't match the visual position (bidi mismatch)
|
|
158
|
+
*/
|
|
159
|
+
function isRtlBidiMismatch(range, formatEl, detectedEdge, doc) {
|
|
160
|
+
if (!range.collapsed || !formatEl) return false;
|
|
161
|
+
|
|
162
|
+
const caretRect = range.getBoundingClientRect();
|
|
163
|
+
if (caretRect.height <= 0) return false;
|
|
164
|
+
|
|
165
|
+
const contentRange = doc.createRange();
|
|
166
|
+
contentRange.selectNodeContents(formatEl);
|
|
167
|
+
|
|
168
|
+
const contentRect = contentRange.getBoundingClientRect();
|
|
169
|
+
if (contentRect.width <= 2) return false;
|
|
170
|
+
|
|
171
|
+
// In RTL: content left = visual end, content right = visual start
|
|
172
|
+
// 'front' mismatch: logically at front (offset 0) but caret at left = visual end
|
|
173
|
+
// 'end' mismatch: logically at end (offset=length) but caret at right = visual start
|
|
174
|
+
return detectedEdge === 'front' ? caretRect.left <= contentRect.left + 2 : caretRect.left >= contentRect.right - 2;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { hardDelete, cleanRemovedTags, isUneditableNode, setDefaultLine, isRtlBidiMismatch };
|
|
@@ -20,6 +20,10 @@ import { A } from '../actions';
|
|
|
20
20
|
export function reduceArrowDown(actions, ports, ctx) {
|
|
21
21
|
const { component } = ports;
|
|
22
22
|
const { formatEl, range, selectionNode, keyCode } = ctx;
|
|
23
|
+
const rtl = ctx.options.get('_rtl');
|
|
24
|
+
|
|
25
|
+
// In RTL, ArrowLeft is forward (toward end on the left), ArrowRight is backward (toward start on the right)
|
|
26
|
+
const hDir = keyCode === 'ArrowLeft' ? (rtl ? 'forward' : 'back') : keyCode === 'ArrowRight' ? (rtl ? 'back' : 'forward') : null;
|
|
23
27
|
|
|
24
28
|
// next component
|
|
25
29
|
let cmponentInfo = null;
|
|
@@ -30,12 +34,24 @@ export function reduceArrowDown(actions, ports, ctx) {
|
|
|
30
34
|
}
|
|
31
35
|
break;
|
|
32
36
|
case 'ArrowLeft' /** left key */:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
case 'ArrowRight' /** right key */:
|
|
38
|
+
if (hDir === 'forward') {
|
|
39
|
+
if (dom.check.isEdgePoint(selectionNode, range.endOffset, 'end')) {
|
|
40
|
+
const nextEl = selectionNode.nextElementSibling || dom.query.getNextDeepestNode(selectionNode);
|
|
41
|
+
if (nextEl) {
|
|
42
|
+
if (component.is(nextEl)) cmponentInfo = component.get(nextEl);
|
|
43
|
+
} else if (component.is(formatEl.nextElementSibling)) {
|
|
44
|
+
cmponentInfo = component.get(formatEl.nextElementSibling);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (hDir === 'back') {
|
|
48
|
+
if (dom.check.isEdgePoint(selectionNode, range.startOffset, 'front')) {
|
|
49
|
+
const prevEl = selectionNode.previousElementSibling || dom.query.getPreviousDeepestNode(selectionNode);
|
|
50
|
+
if (prevEl) {
|
|
51
|
+
if (component.is(prevEl)) cmponentInfo = component.get(prevEl);
|
|
52
|
+
} else if (component.is(formatEl.previousElementSibling)) {
|
|
53
|
+
cmponentInfo = component.get(formatEl.previousElementSibling);
|
|
54
|
+
}
|
|
39
55
|
}
|
|
40
56
|
}
|
|
41
57
|
break;
|
|
@@ -44,16 +60,6 @@ export function reduceArrowDown(actions, ports, ctx) {
|
|
|
44
60
|
cmponentInfo = component.get(formatEl.nextElementSibling);
|
|
45
61
|
}
|
|
46
62
|
break;
|
|
47
|
-
case 'ArrowRight' /** right key */:
|
|
48
|
-
if (dom.check.isEdgePoint(selectionNode, range.endOffset, 'end')) {
|
|
49
|
-
const nextEl = selectionNode.nextElementSibling || dom.query.getNextDeepestNode(selectionNode);
|
|
50
|
-
if (nextEl) {
|
|
51
|
-
if (component.is(nextEl)) cmponentInfo = component.get(nextEl);
|
|
52
|
-
} else if (component.is(formatEl.nextElementSibling)) {
|
|
53
|
-
cmponentInfo = component.get(formatEl.nextElementSibling);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
break;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
if (cmponentInfo && !cmponentInfo.options?.isInputComponent) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dom } from '../../../helper';
|
|
2
|
-
import { cleanRemovedTags, hardDelete, isUneditableNode, setDefaultLine } from '../effects/ruleHelpers';
|
|
2
|
+
import { cleanRemovedTags, hardDelete, isUneditableNode, setDefaultLine, isRtlBidiMismatch } from '../effects/ruleHelpers';
|
|
3
3
|
import { A } from '../actions';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -22,6 +22,8 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
22
22
|
let { formatEl } = ctx;
|
|
23
23
|
|
|
24
24
|
const selectRange = !range.collapsed || range.startContainer !== range.endContainer;
|
|
25
|
+
// RTL bidi guard: if offset 0 is actually at the visual end due to LTR text in RTL line, skip front-edge handling
|
|
26
|
+
const bidiNotFront = options.get('_rtl') && !selectRange && range.startOffset === 0 && isRtlBidiMismatch(range, formatEl, 'front', fc.get('_wd'));
|
|
25
27
|
|
|
26
28
|
actions.push(A.componentDeselect());
|
|
27
29
|
actions.push(A.cacheStyleNode());
|
|
@@ -54,6 +56,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
54
56
|
// closure, default
|
|
55
57
|
if (
|
|
56
58
|
!selectRange &&
|
|
59
|
+
!bidiNotFront &&
|
|
57
60
|
!formatEl.previousElementSibling &&
|
|
58
61
|
range.startOffset === 0 &&
|
|
59
62
|
!selectionNode.previousSibling &&
|
|
@@ -87,7 +90,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
87
90
|
|
|
88
91
|
// clean remove tag
|
|
89
92
|
const startCon = range.startContainer;
|
|
90
|
-
if (formatEl && !formatEl.previousElementSibling && range.startOffset === 0 && startCon.nodeType === 3 && dom.check.isZeroWidth(startCon)) {
|
|
93
|
+
if (formatEl && !bidiNotFront && !formatEl.previousElementSibling && range.startOffset === 0 && startCon.nodeType === 3 && dom.check.isZeroWidth(startCon)) {
|
|
91
94
|
if (cleanRemovedTags(ports, startCon, formatEl) === true) return true;
|
|
92
95
|
}
|
|
93
96
|
|
|
@@ -118,7 +121,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
// format attributes
|
|
121
|
-
if (!selectRange && format.isEdgeLine(range.startContainer, range.startOffset, 'front')) {
|
|
124
|
+
if (!selectRange && !bidiNotFront && format.isEdgeLine(range.startContainer, range.startOffset, 'front')) {
|
|
122
125
|
if (format.isLine(formatEl.previousElementSibling)) {
|
|
123
126
|
actions.push(A.cacheFormatAttrsTemp(formatEl.previousElementSibling.attributes));
|
|
124
127
|
}
|
|
@@ -134,7 +137,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
134
137
|
dom.check.isList(rangeEl) &&
|
|
135
138
|
(dom.check.isListCell(rangeEl.parentElement) || formatEl.previousElementSibling) &&
|
|
136
139
|
(selectionNode === formatEl || (selectionNode.nodeType === 3 && (!selectionNode.previousSibling || dom.check.isList(selectionNode.previousSibling)))) &&
|
|
137
|
-
(format.getLine(range.startContainer, null) !== format.getLine(range.endContainer, null) ? rangeEl.contains(range.startContainer) : range.startOffset === 0 && range.collapsed)
|
|
140
|
+
(format.getLine(range.startContainer, null) !== format.getLine(range.endContainer, null) ? rangeEl.contains(range.startContainer) : range.startOffset === 0 && range.collapsed && !bidiNotFront)
|
|
138
141
|
) {
|
|
139
142
|
if (range.startContainer !== range.endContainer) {
|
|
140
143
|
actions.push(A.prevent());
|
|
@@ -163,7 +166,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
// detach range
|
|
166
|
-
if (!selectRange && range.startOffset === 0) {
|
|
169
|
+
if (!selectRange && range.startOffset === 0 && !bidiNotFront) {
|
|
167
170
|
let detach = true;
|
|
168
171
|
let comm = commonCon;
|
|
169
172
|
while (comm && comm !== rangeEl && !dom.check.isWysiwygFrame(comm)) {
|
|
@@ -185,6 +188,16 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
// empty line adjacent to component (offset-independent — handles RTL caret at offset 1)
|
|
192
|
+
if (!selectRange && formatEl && dom.check.isEmptyLine(formatEl) && component.is(formatEl.previousElementSibling)) {
|
|
193
|
+
const fileComponentInfo = component.get(formatEl.previousElementSibling);
|
|
194
|
+
if (fileComponentInfo) {
|
|
195
|
+
actions.push(A.preventStop());
|
|
196
|
+
actions.push(A.backspaceComponentRemove(false, formatEl.firstChild, formatEl, fileComponentInfo));
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
188
201
|
// component
|
|
189
202
|
if (!selectRange && formatEl && (range.startOffset === 0 || (selectionNode === formatEl ? formatEl.childNodes[range.startOffset] : false))) {
|
|
190
203
|
const isList = dom.check.isListCell(formatEl);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dom } from '../../../helper';
|
|
2
|
-
import { hardDelete, isUneditableNode } from '../effects/ruleHelpers';
|
|
2
|
+
import { hardDelete, isUneditableNode, isRtlBidiMismatch } from '../effects/ruleHelpers';
|
|
3
3
|
import { A } from '../actions';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -18,10 +18,12 @@ import { A } from '../actions';
|
|
|
18
18
|
*/
|
|
19
19
|
export function reduceDeleteDown(actions, ports, ctx) {
|
|
20
20
|
const { format, component } = ports;
|
|
21
|
-
const { range, selectionNode } = ctx;
|
|
21
|
+
const { fc, range, selectionNode } = ctx;
|
|
22
22
|
let { formatEl } = ctx;
|
|
23
23
|
|
|
24
24
|
const selectRange = !range.collapsed || range.startContainer !== range.endContainer;
|
|
25
|
+
// RTL bidi guard: if offset=length is actually at the visual start due to LTR text in RTL line, skip end-edge handling
|
|
26
|
+
const bidiNotEnd = ctx.options.get('_rtl') && !selectRange && range.endOffset >= (range.endContainer.textContent?.length || 0) && isRtlBidiMismatch(range, formatEl, 'end', fc.get('_wd'));
|
|
25
27
|
|
|
26
28
|
actions.push(A.componentDeselect());
|
|
27
29
|
actions.push(A.cacheStyleNode());
|
|
@@ -31,7 +33,7 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
31
33
|
return true;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
if (!selectRange && format.isEdgeLine(range.endContainer, range.endOffset, 'end') && !formatEl.nextSibling) {
|
|
36
|
+
if (!selectRange && !bidiNotEnd && format.isEdgeLine(range.endContainer, range.endOffset, 'end') && !formatEl.nextSibling) {
|
|
35
37
|
actions.push(A.preventStop());
|
|
36
38
|
return false;
|
|
37
39
|
}
|
|
@@ -107,7 +109,7 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
// format attributes
|
|
110
|
-
if (!selectRange && format.isEdgeLine(range.endContainer, range.endOffset, 'end')) {
|
|
112
|
+
if (!selectRange && !bidiNotEnd && format.isEdgeLine(range.endContainer, range.endOffset, 'end')) {
|
|
111
113
|
if (format.isLine(formatEl.nextElementSibling)) {
|
|
112
114
|
actions.push(A.cacheFormatAttrsTemp(formatEl.attributes));
|
|
113
115
|
}
|
|
@@ -122,7 +124,7 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
122
124
|
(selectionNode === formatEl ||
|
|
123
125
|
(selectionNode.nodeType === 3 &&
|
|
124
126
|
(!selectionNode.nextSibling || dom.check.isList(selectionNode.nextSibling)) &&
|
|
125
|
-
(format.getLine(range.startContainer, null) !== format.getLine(range.endContainer, null) ? rangeEl.contains(range.endContainer) : range.endOffset === selectionNode.textContent.length && range.collapsed)))
|
|
127
|
+
(format.getLine(range.startContainer, null) !== format.getLine(range.endContainer, null) ? rangeEl.contains(range.endContainer) : range.endOffset === selectionNode.textContent.length && range.collapsed && !bidiNotEnd)))
|
|
126
128
|
) {
|
|
127
129
|
actions.push(A.deleteListRemoveNested(range, formatEl, rangeEl));
|
|
128
130
|
return true;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dom } from '../../../helper';
|
|
2
|
+
import { isRtlBidiMismatch } from '../effects/ruleHelpers';
|
|
2
3
|
import { A } from '../actions';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -40,8 +41,24 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
if (!shift) {
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
let formatEndEdge = format.isEdgeLine(range.endContainer, range.endOffset, 'end');
|
|
45
|
+
let formatStartEdge = format.isEdgeLine(range.startContainer, range.startOffset, 'front');
|
|
46
|
+
|
|
47
|
+
// RTL bidi edge correction: when LTR text (e.g. numbers) is inside an RTL line,
|
|
48
|
+
// the browser places the caret at the logically opposite offset at bidi boundaries.
|
|
49
|
+
let bidiSwapped = false;
|
|
50
|
+
if (ctx.options.get('_rtl') && formatEl && range.collapsed && formatStartEdge !== formatEndEdge) {
|
|
51
|
+
const _wd = ctx.fc.get('_wd');
|
|
52
|
+
if (formatStartEdge && isRtlBidiMismatch(range, formatEl, 'front', _wd)) {
|
|
53
|
+
formatStartEdge = false;
|
|
54
|
+
formatEndEdge = true;
|
|
55
|
+
bidiSwapped = true;
|
|
56
|
+
} else if (formatEndEdge && isRtlBidiMismatch(range, formatEl, 'end', _wd)) {
|
|
57
|
+
formatEndEdge = false;
|
|
58
|
+
formatStartEdge = true;
|
|
59
|
+
bidiSwapped = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
45
62
|
|
|
46
63
|
// add default format line
|
|
47
64
|
if (formatEndEdge && (/^H[1-6]$/i.test(formatEl.nodeName) || /^HR$/i.test(formatEl.nodeName))) {
|
|
@@ -115,7 +132,7 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
115
132
|
// set format attrs - edge
|
|
116
133
|
if (range.collapsed && (formatStartEdge || formatEndEdge)) {
|
|
117
134
|
ports.enterPrevent(e);
|
|
118
|
-
actions.push(A.enterFormatBreakAtEdge(formatEl, selectionNode, formatStartEdge, formatEndEdge));
|
|
135
|
+
actions.push(A.enterFormatBreakAtEdge(formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped));
|
|
119
136
|
actions.push(A.enterScrollTo(range));
|
|
120
137
|
return true;
|
|
121
138
|
}
|
|
@@ -125,6 +142,19 @@ export function reduceEnterDown(actions, ports, ctx) {
|
|
|
125
142
|
|
|
126
143
|
/** @type {HTMLElement} */
|
|
127
144
|
if (selectRange) {
|
|
145
|
+
// RTL bidi edge correction for selection ranges (formatStartEdge only)
|
|
146
|
+
// formatEndEdge insert-after behavior is already correct for RTL selections
|
|
147
|
+
if (ctx.options.get('_rtl') && formatStartEdge && !formatEndEdge) {
|
|
148
|
+
const _wd = ctx.fc.get('_wd');
|
|
149
|
+
const testRange = _wd.createRange();
|
|
150
|
+
testRange.setStart(range.startContainer, range.startOffset);
|
|
151
|
+
testRange.collapse(true);
|
|
152
|
+
if (isRtlBidiMismatch(testRange, formatEl, 'front', _wd)) {
|
|
153
|
+
formatStartEdge = false;
|
|
154
|
+
formatEndEdge = true;
|
|
155
|
+
bidiSwapped = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
128
158
|
actions.push(A.enterFormatBreakWithSelection(formatEl, range, formatStartEdge, formatEndEdge));
|
|
129
159
|
} else {
|
|
130
160
|
actions.push(A.enterFormatBreakAtCursor(formatEl, range));
|
|
@@ -200,14 +200,27 @@ class UIManager {
|
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
202
|
* @description Set direction to `rtl` or `ltr`.
|
|
203
|
-
* @param {
|
|
203
|
+
* @param {"rtl"|"ltr"} dir `rtl` or `ltr`
|
|
204
204
|
*/
|
|
205
205
|
setDir(dir) {
|
|
206
206
|
const rtl = dir === 'rtl';
|
|
207
207
|
if (this.#options.get('_rtl') === rtl) return;
|
|
208
208
|
|
|
209
|
+
const prevDir = this.#options.get('textDirection');
|
|
210
|
+
const prevEditableClass = this.#options.get('_editableClass');
|
|
211
|
+
const prevPrintClass = this.#options.get('printClass');
|
|
212
|
+
|
|
209
213
|
try {
|
|
210
214
|
this.#options.set('_rtl', rtl);
|
|
215
|
+
this.#options.set('textDirection', dir);
|
|
216
|
+
|
|
217
|
+
// update _editableClass / printClass
|
|
218
|
+
const editableClass = rtl ? this.#options.get('_editableClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('_editableClass').replace(/\s*se-rtl/, '');
|
|
219
|
+
this.#options.set('_editableClass', editableClass);
|
|
220
|
+
if (this.#options.get('printClass')) {
|
|
221
|
+
this.#options.set('printClass', rtl ? this.#options.get('printClass').replace(/\s*se-rtl/, '') + ' se-rtl' : this.#options.get('printClass').replace(/\s*se-rtl/, ''));
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
this.offCurrentController();
|
|
212
225
|
|
|
213
226
|
const fc = this.#frameContext;
|
|
@@ -221,11 +234,13 @@ class UIManager {
|
|
|
221
234
|
if (rtl) {
|
|
222
235
|
this.#contextProvider.applyToRoots((e) => {
|
|
223
236
|
dom.utils.addClass([e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')], 'se-rtl');
|
|
237
|
+
e.get('wysiwyg').dir = 'rtl';
|
|
224
238
|
});
|
|
225
239
|
dom.utils.addClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
|
|
226
240
|
} else {
|
|
227
241
|
this.#contextProvider.applyToRoots((e) => {
|
|
228
242
|
dom.utils.removeClass([e.get('topArea'), e.get('wysiwyg'), e.get('documentTypePageMirror')], 'se-rtl');
|
|
243
|
+
e.get('wysiwyg').removeAttribute('dir');
|
|
229
244
|
});
|
|
230
245
|
dom.utils.removeClass([this.#carrierWrapper, toolbarWrapper, statusbarWrapper], 'se-rtl');
|
|
231
246
|
}
|
|
@@ -255,6 +270,12 @@ class UIManager {
|
|
|
255
270
|
|
|
256
271
|
this.#activeDirBtn(rtl);
|
|
257
272
|
|
|
273
|
+
// reverse toolbar buttons
|
|
274
|
+
this.#reverseToolbarButtons(this.#context.get('toolbar_buttonTray'));
|
|
275
|
+
if (this.#context.has('toolbar_sub_buttonTray')) {
|
|
276
|
+
this.#reverseToolbarButtons(this.#context.get('toolbar_sub_buttonTray'));
|
|
277
|
+
}
|
|
278
|
+
|
|
258
279
|
// document type
|
|
259
280
|
if (fc.has('documentType_use_header')) {
|
|
260
281
|
if (rtl) fc.get('wrapper').appendChild(fc.get('documentTypeInner'));
|
|
@@ -269,6 +290,9 @@ class UIManager {
|
|
|
269
290
|
else if (this.#store.mode.isSubBalloon) this.#$.subToolbar._showBalloon();
|
|
270
291
|
} catch (e) {
|
|
271
292
|
this.#options.set('_rtl', !rtl);
|
|
293
|
+
this.#options.set('textDirection', prevDir);
|
|
294
|
+
this.#options.set('_editableClass', prevEditableClass);
|
|
295
|
+
if (prevPrintClass !== null) this.#options.set('printClass', prevPrintClass);
|
|
272
296
|
console.warn(`[SUNEDITOR.ui.setDir.fail] ${e.toString()}`);
|
|
273
297
|
}
|
|
274
298
|
|
|
@@ -702,6 +726,20 @@ class UIManager {
|
|
|
702
726
|
}
|
|
703
727
|
}
|
|
704
728
|
|
|
729
|
+
/**
|
|
730
|
+
* @description Reverse the order of toolbar button groups (excluding the more-layer).
|
|
731
|
+
* @param {HTMLElement} buttonTray - The `.se-btn-tray` element.
|
|
732
|
+
*/
|
|
733
|
+
#reverseToolbarButtons(buttonTray) {
|
|
734
|
+
if (!buttonTray) return;
|
|
735
|
+
const moreLayer = buttonTray.querySelector('.se-toolbar-more-layer');
|
|
736
|
+
const children = Array.from(buttonTray.children).filter((c) => c !== moreLayer);
|
|
737
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
738
|
+
buttonTray.appendChild(children[i]);
|
|
739
|
+
}
|
|
740
|
+
if (moreLayer) buttonTray.appendChild(moreLayer);
|
|
741
|
+
}
|
|
742
|
+
|
|
705
743
|
/**
|
|
706
744
|
* @internal
|
|
707
745
|
* @description Set the disabled button list
|
|
@@ -361,8 +361,21 @@ export function CreateShortcuts(command, button, values, keyMap, rc, reverseKeys
|
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* @description Append tooltip span
|
|
366
|
+
* @param {Element} tooptipBtn
|
|
367
|
+
* @param {boolean} shift
|
|
368
|
+
* @param {string} shortcut
|
|
369
|
+
*/
|
|
364
370
|
function _addTooltip(tooptipBtn, shift, shortcut) {
|
|
365
|
-
|
|
371
|
+
const tooltip = dom.utils.createElement('SPAN', { class: 'se-shortcut' }, env.cmdIcon + (shift ? env.shiftIcon : '') + '+<span class="se-shortcut-key">' + shortcut + '</span>');
|
|
372
|
+
const prevTooltip = tooptipBtn.querySelector('.se-shortcut');
|
|
373
|
+
|
|
374
|
+
if (prevTooltip) {
|
|
375
|
+
tooptipBtn.replaceChild(tooltip, prevTooltip);
|
|
376
|
+
} else {
|
|
377
|
+
tooptipBtn.appendChild(tooltip);
|
|
378
|
+
}
|
|
366
379
|
}
|
|
367
380
|
|
|
368
381
|
/**
|
|
@@ -937,6 +950,7 @@ function _initTargetElements(key, options, topDiv, targetOptions) {
|
|
|
937
950
|
|
|
938
951
|
if (!targetOptions.get('iframe')) {
|
|
939
952
|
wysiwygDiv.setAttribute('contenteditable', 'true');
|
|
953
|
+
if (options.get('_rtl')) wysiwygDiv.dir = 'rtl';
|
|
940
954
|
wysiwygDiv.className += ' ' + options.get('_editableClass');
|
|
941
955
|
wysiwygDiv.style.cssText = editorStyles.frame + editorStyles.editor;
|
|
942
956
|
} else {
|
|
@@ -31,7 +31,7 @@ export namespace A {
|
|
|
31
31
|
function enterFormatCleanBrAndZWS(selectionNode: Node, selectionFormat: boolean, brBlock: Element, children: NodeList, offset: number): Action;
|
|
32
32
|
function enterFormatInsertBrHtml(brBlock: Element, range: Range, wSelection: Selection, offset: number): Action;
|
|
33
33
|
function enterFormatInsertBrNode(wSelection: Selection): Action;
|
|
34
|
-
function enterFormatBreakAtEdge(formatEl: Element, selectionNode: Node, formatStartEdge: boolean, formatEndEdge: boolean): Action;
|
|
34
|
+
function enterFormatBreakAtEdge(formatEl: Element, selectionNode: Node, formatStartEdge: boolean, formatEndEdge: boolean, bidiSwapped?: boolean): Action;
|
|
35
35
|
function enterFormatBreakWithSelection(formatEl: Element, range: Range, formatStartEdge: boolean, formatEndEdge: boolean): Action;
|
|
36
36
|
function enterFormatBreakAtCursor(formatEl: Element, range: Range): Action;
|
|
37
37
|
function enterFigcaptionExitInList(formatEl: Element): Action;
|
|
@@ -42,7 +42,7 @@ declare const _default: {
|
|
|
42
42
|
/** @action enterFormatInsertBrNode */
|
|
43
43
|
'enter.format.insertBrNode': ({ ports }: EffectContext_keydown, { wSelection }: any) => void;
|
|
44
44
|
/** @action enterFormatBreakAtEdge */
|
|
45
|
-
'enter.format.breakAtEdge': ({ ports, ctx }: EffectContext_keydown, { formatEl, selectionNode, formatStartEdge, formatEndEdge }: any) => void;
|
|
45
|
+
'enter.format.breakAtEdge': ({ ports, ctx }: EffectContext_keydown, { formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped }: any) => void;
|
|
46
46
|
/** @action enterFormatBreakWithSelection */
|
|
47
47
|
'enter.format.breakWithSelection': ({ ports, ctx }: EffectContext_keydown, { formatEl, range, formatStartEdge, formatEndEdge }: any) => void;
|
|
48
48
|
/** @action enterFormatBreakAtCursor */
|
|
@@ -34,3 +34,15 @@ export function isUneditableNode(ports: EventPorts, range: Range, isFront: boole
|
|
|
34
34
|
* @returns {void}
|
|
35
35
|
*/
|
|
36
36
|
export function setDefaultLine(ports: EventPorts, lineTagName: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* @description Detects if a detected logical edge is incorrect due to bidi text direction mismatch in RTL mode.
|
|
39
|
+
* When LTR text (numbers, Latin) is inside an RTL line, the browser may place the caret at offset 0
|
|
40
|
+
* for the visual end or offset=length for the visual start. This function compares the caret's visual
|
|
41
|
+
* position against the content boundaries to detect such mismatches.
|
|
42
|
+
* @param {Range} range - The current collapsed range
|
|
43
|
+
* @param {HTMLElement} formatEl - The format/line element
|
|
44
|
+
* @param {'front'|'end'} detectedEdge - The edge detected by logical offset check
|
|
45
|
+
* @param {Document} doc - The document object
|
|
46
|
+
* @returns {boolean} true if the detected edge doesn't match the visual position (bidi mismatch)
|
|
47
|
+
*/
|
|
48
|
+
export function isRtlBidiMismatch(range: Range, formatEl: HTMLElement, detectedEdge: 'front' | 'end', doc: Document): boolean;
|