suneditor 3.1.0 → 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/dom/format.js +5 -1
- package/src/core/logic/panel/menu.js +74 -3
- package/src/core/logic/shell/ui.js +39 -1
- package/src/core/section/constructor.js +15 -1
- package/src/plugins/field/autocomplete.js +42 -5
- 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/types/plugins/field/autocomplete.d.ts +84 -10
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));
|
|
@@ -766,7 +766,11 @@ class Format {
|
|
|
766
766
|
* @returns {node is HTMLElement}
|
|
767
767
|
*/
|
|
768
768
|
isEdgeLine(node, offset, dir) {
|
|
769
|
-
if (
|
|
769
|
+
if (dir === 'front') {
|
|
770
|
+
if (offset > 0) return false;
|
|
771
|
+
} else {
|
|
772
|
+
if (node?.textContent?.length > offset) return false;
|
|
773
|
+
}
|
|
770
774
|
|
|
771
775
|
let result = false;
|
|
772
776
|
const siblingType = dir === 'front' ? 'previousSibling' : 'nextSibling';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { dom, converter } from '../../../helper';
|
|
1
|
+
import { dom, converter, env } from '../../../helper';
|
|
2
|
+
|
|
3
|
+
const { isMobile, _w } = env;
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @description Dropdown and container menu management class
|
|
@@ -22,6 +24,9 @@ class Menu {
|
|
|
22
24
|
#bindMenu_mouseout = null;
|
|
23
25
|
#menuBtn = null;
|
|
24
26
|
#menuContainer = null;
|
|
27
|
+
#deferredShowTimer = null;
|
|
28
|
+
#viewportListener = null;
|
|
29
|
+
#visualViewport = null;
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* @constructor
|
|
@@ -67,6 +72,7 @@ class Menu {
|
|
|
67
72
|
// eventManager member (viewport)
|
|
68
73
|
this.#menuBtn = null;
|
|
69
74
|
this.#menuContainer = null;
|
|
75
|
+
this.#visualViewport = _w.visualViewport || null;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
/**
|
|
@@ -109,7 +115,12 @@ class Menu {
|
|
|
109
115
|
this.currentDropdownType = btnEl.getAttribute('data-type');
|
|
110
116
|
const menu = (this.currentDropdown = this.targetMap[dropdownName]);
|
|
111
117
|
this.currentDropdownActiveButton = btnEl;
|
|
112
|
-
|
|
118
|
+
|
|
119
|
+
if (isMobile) {
|
|
120
|
+
this.#deferMenuShow(btnEl, menu);
|
|
121
|
+
} else {
|
|
122
|
+
this.#setMenuPosition(btnEl, menu);
|
|
123
|
+
}
|
|
113
124
|
|
|
114
125
|
this.#bindClose_dropdown_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.mousedown, false);
|
|
115
126
|
if (this.#dropdownCommands.includes(dropdownName)) {
|
|
@@ -131,6 +142,7 @@ class Menu {
|
|
|
131
142
|
* @description Closes the currently open dropdown menu.
|
|
132
143
|
*/
|
|
133
144
|
dropdownOff() {
|
|
145
|
+
this.#clearDeferredShow();
|
|
134
146
|
this.#removeGlobalEvent();
|
|
135
147
|
if (IsFree(this.currentDropdownType)) this.currentDropdownPlugin?.off?.();
|
|
136
148
|
|
|
@@ -189,7 +201,13 @@ class Menu {
|
|
|
189
201
|
|
|
190
202
|
this.currentContainerActiveButton = /** @type {HTMLButtonElement} */ (button);
|
|
191
203
|
const containerName = (this.currentContainerName = this.currentContainerActiveButton.getAttribute('data-command'));
|
|
192
|
-
this
|
|
204
|
+
this.currentContainer = this.targetMap[containerName];
|
|
205
|
+
|
|
206
|
+
if (isMobile) {
|
|
207
|
+
this.#deferMenuShow(button, this.currentContainer);
|
|
208
|
+
} else {
|
|
209
|
+
this.#setMenuPosition(button, this.currentContainer);
|
|
210
|
+
}
|
|
193
211
|
|
|
194
212
|
this.#bindClose_cons_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.containerDown, false);
|
|
195
213
|
|
|
@@ -201,6 +219,7 @@ class Menu {
|
|
|
201
219
|
* @description Closes the currently open menu container.
|
|
202
220
|
*/
|
|
203
221
|
containerOff() {
|
|
222
|
+
this.#clearDeferredShow();
|
|
204
223
|
this.#removeGlobalEvent();
|
|
205
224
|
|
|
206
225
|
if (this.currentContainer) {
|
|
@@ -231,6 +250,8 @@ class Menu {
|
|
|
231
250
|
*/
|
|
232
251
|
__restoreMenuPosition() {
|
|
233
252
|
if (!this.#menuBtn || !this.#menuContainer) return;
|
|
253
|
+
// skip if deferred show is pending — it will handle positioning after viewport settles
|
|
254
|
+
if (this.#viewportListener || this.#deferredShowTimer) return;
|
|
234
255
|
this.#setMenuPosition(this.#menuBtn, this.#menuContainer);
|
|
235
256
|
}
|
|
236
257
|
|
|
@@ -253,6 +274,56 @@ class Menu {
|
|
|
253
274
|
this.#menuContainer = menu;
|
|
254
275
|
}
|
|
255
276
|
|
|
277
|
+
/**
|
|
278
|
+
* @description Defer menu display on mobile until viewport settles after keyboard dismiss.
|
|
279
|
+
* @param {Node} element Button element
|
|
280
|
+
* @param {HTMLElement} menu Menu element
|
|
281
|
+
*/
|
|
282
|
+
#deferMenuShow(element, menu) {
|
|
283
|
+
this.#clearDeferredShow();
|
|
284
|
+
|
|
285
|
+
menu.style.display = 'none';
|
|
286
|
+
dom.utils.addClass(element.parentElement.children, 'on');
|
|
287
|
+
this.#menuBtn = element;
|
|
288
|
+
this.#menuContainer = menu;
|
|
289
|
+
|
|
290
|
+
let resolved = false;
|
|
291
|
+
const show = (delay) => {
|
|
292
|
+
if (resolved) return;
|
|
293
|
+
resolved = true;
|
|
294
|
+
this.#clearDeferredShow();
|
|
295
|
+
this.#deferredShowTimer = _w.setTimeout(() => {
|
|
296
|
+
this.#deferredShowTimer = null;
|
|
297
|
+
if (this.#menuBtn === element) {
|
|
298
|
+
this.#setMenuPosition(element, menu);
|
|
299
|
+
}
|
|
300
|
+
}, delay);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (this.#visualViewport) {
|
|
304
|
+
// listen for viewport resize (keyboard dismiss) — small delay to let viewport settle
|
|
305
|
+
this.#viewportListener = () => show(10);
|
|
306
|
+
this.#visualViewport.addEventListener('resize', this.#viewportListener, { once: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// fallback if no viewport change occurs (keyboard already hidden or no visualViewport)
|
|
310
|
+
this.#deferredShowTimer = _w.setTimeout(() => show(0), 50);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @description Clear deferred show timer and viewport listener.
|
|
315
|
+
*/
|
|
316
|
+
#clearDeferredShow() {
|
|
317
|
+
if (this.#deferredShowTimer) {
|
|
318
|
+
_w.clearTimeout(this.#deferredShowTimer);
|
|
319
|
+
this.#deferredShowTimer = null;
|
|
320
|
+
}
|
|
321
|
+
if (this.#viewportListener) {
|
|
322
|
+
this.#visualViewport?.removeEventListener('resize', this.#viewportListener);
|
|
323
|
+
this.#viewportListener = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
256
327
|
/**
|
|
257
328
|
* @description Check if the element is part of a more layer
|
|
258
329
|
* @param {Node} element The element to check
|