suneditor 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/suneditor-contents.min.css +1 -1
  3. package/dist/suneditor.min.css +1 -1
  4. package/dist/suneditor.min.js +1 -1
  5. package/package.json +10 -7
  6. package/src/assets/design/size.css +2 -1
  7. package/src/assets/suneditor.css +75 -23
  8. package/src/core/editor.js +1 -0
  9. package/src/core/event/actions/index.js +2 -1
  10. package/src/core/event/effects/keydown.registry.js +3 -1
  11. package/src/core/event/effects/ruleHelpers.js +30 -1
  12. package/src/core/event/handlers/handler_ww_dragDrop.js +20 -0
  13. package/src/core/event/rules/keydown.rule.arrow.js +22 -16
  14. package/src/core/event/rules/keydown.rule.backspace.js +18 -5
  15. package/src/core/event/rules/keydown.rule.delete.js +7 -5
  16. package/src/core/event/rules/keydown.rule.enter.js +33 -3
  17. package/src/core/logic/panel/menu.js +4 -0
  18. package/src/core/logic/shell/ui.js +52 -3
  19. package/src/core/schema/options.js +1 -1
  20. package/src/core/section/constructor.js +44 -13
  21. package/src/core/section/documentType.js +55 -29
  22. package/src/modules/contract/Browser.js +3 -0
  23. package/src/modules/contract/Controller.js +3 -0
  24. package/src/modules/contract/Figure.js +23 -4
  25. package/src/modules/contract/Modal.js +2 -0
  26. package/src/plugins/dropdown/table/services/table.style.js +21 -12
  27. package/types/core/event/actions/index.d.ts +1 -1
  28. package/types/core/event/effects/keydown.registry.d.ts +1 -1
  29. package/types/core/event/effects/ruleHelpers.d.ts +12 -0
  30. package/types/core/logic/shell/ui.d.ts +2 -2
  31. package/types/core/schema/options.d.ts +2 -2
  32. package/types/core/section/documentType.d.ts +17 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suneditor",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "Vanilla JavaScript based WYSIWYG web editor",
5
5
  "author": "Yi JiHong",
6
6
  "license": "MIT",
@@ -49,16 +49,19 @@
49
49
  "engines": {
50
50
  "node": ">=14.0.0"
51
51
  },
52
+ "overrides": {
53
+ "uuid": ">=14.0.0"
54
+ },
52
55
  "scripts": {
53
56
  "dev": "webpack-dev-server --config webpack/dev.js",
54
57
  "start": "npm run dev",
55
58
  "build:dev": "cross-env NODE_ENV=development webpack --config webpack/builder.js && cross-env NODE_ENV=development webpack --config webpack/builder-contents.js && rm -f dist/_suneditor-contents.js",
56
59
  "build:prod": "cross-env NODE_ENV=production webpack --config webpack/builder.js && cross-env NODE_ENV=production webpack --config webpack/builder-contents.js && rm -f dist/_suneditor-contents.js",
57
60
  "lint:type": "npx tsc --noEmit",
58
- "lint:fix-js": "npx eslint \"src/**/*.js\" --fix",
61
+ "lint:fix-js": "npx eslint src/ --fix",
59
62
  "lint:fix-ts": "npx eslint types/ --fix",
60
- "lint:fix-all": "npx eslint \"src/**/*.js\" types/ --fix",
61
- "lint": "npx eslint \"src/**/*.js\" types/ && npm run lint:type && npm run check:arch",
63
+ "lint:fix-all": "npx eslint src/ types/ --fix",
64
+ "lint": "npx eslint src/ types/ && npm run lint:type && npm run check:arch",
62
65
  "ts-build": "npm run check:inject && (npx tsc || true) && barrelsby --config .barrelsby.json && node scripts/ts-build/format-index.cjs && node scripts/ts-build/fix-langs.cjs && node scripts/ts-build/wrap-dts.cjs && node scripts/ts-build/rename-index.cjs && node scripts/ts-build/interfaces-convert.cjs && node scripts/ts-build/gen-options-dts.cjs && node scripts/ts-build/gen-css-dts.cjs && node scripts/ts-build/inject-typedef-import.cjs && npm run lint:fix-ts",
63
66
  "test": "jest --silent",
64
67
  "test:watch": "jest --watch",
@@ -125,13 +128,13 @@
125
128
  "webpack": "^5.102.0",
126
129
  "webpack-bundle-analyzer": "^4.10.2",
127
130
  "webpack-cli": "^6.0.1",
128
- "webpack-dev-server": "^5.2.2",
131
+ "webpack-dev-server": "^5.2.3",
129
132
  "webpack-merge": "^6.0.1"
130
133
  },
131
134
  "browserslist": [
132
135
  "chrome >= 119",
133
136
  "edge >= 119",
134
- "firefox >= 121",
137
+ "firefox >= 125",
135
138
  "safari >= 17.2",
136
139
  "opera >= 105",
137
140
  "samsung >= 23",
@@ -168,4 +171,4 @@
168
171
  "web editor",
169
172
  "browser editor"
170
173
  ]
171
- }
174
+ }
@@ -8,7 +8,8 @@
8
8
 
9
9
  /** --------------------------- layout - [size] ----------- */
10
10
  /** main, common */
11
- --se-border-radius: 2px;
11
+ --se-border-radius: 3px;
12
+ --se-border-radius-lg: 6px;
12
13
  --se-min-height: 65px;
13
14
  --se-scroll-padding: 2em;
14
15
 
@@ -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);
@@ -1137,6 +1149,26 @@
1137
1149
  z-index: 2147483646;
1138
1150
  }
1139
1151
 
1152
+ /** --- popover UA override ---------------------------------------------------- */
1153
+ .sun-editor .se-controller[popover],
1154
+ .sun-editor.sun-editor-carrier-wrapper .se-drag-cursor[popover] {
1155
+ inset: unset;
1156
+ margin: 0;
1157
+ }
1158
+
1159
+ .sun-editor .se-modal-area[popover],
1160
+ .sun-editor .se-back-wrapper[popover],
1161
+ .sun-editor .se-loading-box[popover],
1162
+ .sun-editor .se-toast[popover],
1163
+ .sun-editor .se-browser[popover],
1164
+ .sun-editor .se-menu-tray[popover] {
1165
+ background: none;
1166
+ border: none;
1167
+ padding: 0;
1168
+ overflow: visible;
1169
+ color: inherit;
1170
+ }
1171
+
1140
1172
  /** --- dropdown layer ---------------------------------------------------------- */
1141
1173
  .sun-editor .se-dropdown {
1142
1174
  overflow-x: hidden;
@@ -1156,7 +1188,7 @@
1156
1188
  height: auto;
1157
1189
  z-index: 5;
1158
1190
  border: 1px solid var(--se-main-divider-color);
1159
- border-radius: var(--se-border-radius);
1191
+ border-radius: var(--se-border-radius-lg);
1160
1192
  padding: 4px;
1161
1193
  background-color: var(--se-main-background-color);
1162
1194
  box-shadow: 0 3px 9px var(--se-shadow-layer-color);
@@ -1189,7 +1221,8 @@
1189
1221
 
1190
1222
  /* dropdown layer - basic list */
1191
1223
  .sun-editor .se-list-inner .se-list-basic li {
1192
- width: 100%;
1224
+ width: auto;
1225
+ max-width: 100%;
1193
1226
  }
1194
1227
  .sun-editor .se-list-inner input {
1195
1228
  border-radius: 0;
@@ -1563,6 +1596,19 @@
1563
1596
  stroke-width: 2px;
1564
1597
  }
1565
1598
 
1599
+ .sun-editor .se-list-layer .se-selector-color .se-color-pallet:first-child li:first-child button {
1600
+ border-top-left-radius: var(--se-border-radius);
1601
+ }
1602
+ .sun-editor .se-list-layer .se-selector-color .se-color-pallet:first-child li:last-child button {
1603
+ border-top-right-radius: var(--se-border-radius);
1604
+ }
1605
+ .sun-editor .se-list-layer .se-selector-color .se-color-pallet:last-of-type li:first-child button {
1606
+ border-bottom-left-radius: var(--se-border-radius);
1607
+ }
1608
+ .sun-editor .se-list-layer .se-selector-color .se-color-pallet:last-of-type li:last-child button {
1609
+ border-bottom-right-radius: var(--se-border-radius);
1610
+ }
1611
+
1566
1612
  /* --- hue slider -------------------------------------------------------------- */
1567
1613
  .sun-editor .se-hue-slider {
1568
1614
  padding: 14px;
@@ -1997,7 +2043,7 @@
1997
2043
  -webkit-background-clip: padding-box;
1998
2044
  background-clip: padding-box;
1999
2045
  border: 1px solid rgba(0, 0, 0, 0.2);
2000
- border-radius: var(--se-border-radius);
2046
+ border-radius: var(--se-border-radius-lg);
2001
2047
  outline: 0;
2002
2048
  box-shadow: 0 3px 9px var(--se-shadow-layer-color);
2003
2049
  }
@@ -2542,6 +2588,12 @@
2542
2588
  display: none;
2543
2589
  overflow: visible;
2544
2590
  z-index: 2147483646;
2591
+ }
2592
+
2593
+ .sun-editor .se-controller[popover]:popover-open {
2594
+ position: absolute;
2595
+ inset: unset;
2596
+ margin: 0;
2545
2597
  padding: 2px 2px 0 2px;
2546
2598
  margin: 0;
2547
2599
  border: 1px solid var(--se-controller-border-color);
@@ -2558,6 +2610,7 @@
2558
2610
  color: var(--se-controller-color);
2559
2611
  -webkit-background-clip: padding-box;
2560
2612
  background-clip: padding-box;
2613
+ border-radius: var(--se-border-radius);
2561
2614
  line-break: auto;
2562
2615
  }
2563
2616
 
@@ -2582,13 +2635,13 @@
2582
2635
 
2583
2636
  .sun-editor-editable[contenteditable='true'] figure figcaption {
2584
2637
  display: block;
2585
- z-index: 2;
2638
+ z-index: 11;
2586
2639
  }
2587
2640
 
2588
2641
  .sun-editor-editable[contenteditable='true']:not(.se-read-only) figure:not(.se-input-component)::after {
2589
2642
  position: absolute;
2590
2643
  content: '';
2591
- z-index: 1;
2644
+ z-index: 10;
2592
2645
  top: 0;
2593
2646
  left: 0;
2594
2647
  right: 0;
@@ -2960,10 +3013,10 @@
2960
3013
  overflow-x: visible;
2961
3014
  overflow: visible;
2962
3015
  background-color: var(--se-main-background-color);
2963
- border-radius: var(--se-border-radius);
2964
- padding: 2px 0;
3016
+ border-radius: var(--se-border-radius-lg);
3017
+ padding: 4px 0;
2965
3018
  margin: 0;
2966
- border: 1px solid var(--se-main-outline-color);
3019
+ border: 1px solid var(--se-main-divider-color);
2967
3020
  box-shadow: 0 3px 9px var(--se-shadow-layer-color);
2968
3021
  outline: 0 none;
2969
3022
  }
@@ -2990,8 +3043,10 @@
2990
3043
  min-height: 28px;
2991
3044
  font-size: var(--se-btn-font-size);
2992
3045
  padding: 0 6px;
2993
- margin: 2px 0;
3046
+ margin: 1px 4px;
2994
3047
  cursor: pointer;
3048
+ border-radius: var(--se-border-radius);
3049
+ transition: background-color 0.08s ease;
2995
3050
  }
2996
3051
 
2997
3052
  .sun-editor .se-select-menu .se-select-item button {
@@ -3011,21 +3066,12 @@
3011
3066
 
3012
3067
  .sun-editor .se-select-menu.se-select-menu-mouse-move .se-select-item:hover,
3013
3068
  .sun-editor .se-select-menu:not(.se-select-menu-mouse-move) .se-select-item.active {
3014
- border-color: var(--se-active-light3-color) !important;
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;
3069
+ background-color: var(--se-hover-light-color);
3018
3070
  }
3019
3071
 
3020
3072
  .sun-editor .se-select-menu.se-select-menu-mouse-move .se-select-item:active,
3021
3073
  .sun-editor .se-select-menu.se-select-menu-mouse-move .se-select-item.__se__active {
3022
- background-color: var(--se-active-light4-color);
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;
3074
+ background-color: var(--se-hover-light2-color);
3029
3075
  }
3030
3076
 
3031
3077
  .sun-editor .se-modal-form-files .se-select-menu {
@@ -3086,7 +3132,7 @@
3086
3132
  -webkit-background-clip: padding-box;
3087
3133
  background-clip: padding-box;
3088
3134
  border: 1px solid rgba(0, 0, 0, 0.2);
3089
- border-radius: var(--se-border-radius);
3135
+ border-radius: var(--se-border-radius-lg);
3090
3136
  outline: 0;
3091
3137
  box-shadow: 0 3px 9px var(--se-shadow-layer-color);
3092
3138
  }
@@ -3943,6 +3989,12 @@
3943
3989
  .sun-editor.se-rtl .se-find-replace-row {
3944
3990
  justify-content: flex-start;
3945
3991
  }
3992
+ .sun-editor.se-rtl .se-find-replace-input {
3993
+ direction: rtl;
3994
+ }
3995
+ .sun-editor.se-rtl .se-input-form {
3996
+ direction: rtl;
3997
+ }
3946
3998
 
3947
3999
  /* statusbar */
3948
4000
  .sun-editor.se-rtl .se-status-bar {
@@ -4527,8 +4579,8 @@
4527
4579
  .sun-editor-editable[contenteditable='true'] .se-page-break::after {
4528
4580
  content: '⎯⎯⎯⎯⎯⎯';
4529
4581
  position: absolute;
4530
- top: 1px;
4531
- background: var(--se-main-background-color);
4582
+ top: 0px;
4583
+ background: transparent;
4532
4584
  padding: 0 5px;
4533
4585
  color: var(--se-doc-info-page-background-color);
4534
4586
  font-size: var(--se-main-font-size);
@@ -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
- export { hardDelete, cleanRemovedTags, isUneditableNode, setDefaultLine };
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 };
@@ -46,7 +46,17 @@ export function OnDragOver_wysiwyg(fc, dragCursor, _iframeTopArea, _innerToolbar
46
46
  dragCursor.style.top = `${rect.top + _w.scrollY + _offset.y - 5 + frameY}px`;
47
47
  dragCursor.style.height = `${rect.height + 10}px`;
48
48
  dragCursor.style.display = 'block';
49
+ try {
50
+ dragCursor.showPopover?.();
51
+ } catch {
52
+ // ignore
53
+ }
49
54
  } else {
55
+ try {
56
+ dragCursor.hidePopover?.();
57
+ } catch {
58
+ // ignore
59
+ }
50
60
  dragCursor.style.display = 'none';
51
61
  }
52
62
  }
@@ -56,6 +66,11 @@ export function OnDragOver_wysiwyg(fc, dragCursor, _iframeTopArea, _innerToolbar
56
66
  * @param {HTMLElement} dragCursor - Drag cursor element
57
67
  */
58
68
  export function OnDragEnd_wysiwyg(dragCursor) {
69
+ try {
70
+ dragCursor.hidePopover?.();
71
+ } catch {
72
+ // ignore
73
+ }
59
74
  dragCursor.style.display = 'none';
60
75
  }
61
76
 
@@ -109,6 +124,11 @@ export function OnDrop_wysiwyg(fc, dragCursor, e) {
109
124
  this.$.selection.setRange(sc, so, ec, eo);
110
125
  return this._dataTransferAction('drop', e, dataTransfer, fc);
111
126
  } finally {
127
+ try {
128
+ dragCursor.hidePopover?.();
129
+ } catch {
130
+ // ignore
131
+ }
112
132
  dragCursor.style.display = 'none';
113
133
  }
114
134
  }
@@ -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
- if (dom.check.isEdgePoint(selectionNode, range.startOffset, 'front')) {
34
- const prevEl = selectionNode.previousElementSibling || dom.query.getPreviousDeepestNode(selectionNode);
35
- if (prevEl) {
36
- if (component.is(prevEl)) cmponentInfo = component.get(prevEl);
37
- } else if (component.is(formatEl.previousElementSibling)) {
38
- cmponentInfo = component.get(formatEl.previousElementSibling);
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
- const formatEndEdge = format.isEdgeLine(range.endContainer, range.endOffset, 'end');
44
- const formatStartEdge = format.isEdgeLine(range.startContainer, range.startOffset, 'front');
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));
@@ -122,6 +122,7 @@ class Menu {
122
122
  this.#setMenuPosition(btnEl, menu);
123
123
  }
124
124
 
125
+ this.#context.get('menuTray').showPopover?.();
125
126
  this.#bindClose_dropdown_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.mousedown, false);
126
127
  if (this.#dropdownCommands.includes(dropdownName)) {
127
128
  this.menus = converter.nodeListToArray(menu.querySelectorAll('[data-command]'));
@@ -162,6 +163,7 @@ class Menu {
162
163
  }
163
164
  this.currentDropdownActiveButton = null;
164
165
  this.#$.ui.preventToolbarHide(false);
166
+ this.#context.get('menuTray').hidePopover?.();
165
167
  }
166
168
 
167
169
  this.#store.set('_preventBlur', false);
@@ -209,6 +211,7 @@ class Menu {
209
211
  this.#setMenuPosition(button, this.currentContainer);
210
212
  }
211
213
 
214
+ this.#context.get('menuTray').showPopover?.();
212
215
  this.#bindClose_cons_mouse = this.#eventManager.addGlobalEvent('mousedown', this.#globalEventHandler.containerDown, false);
213
216
 
214
217
  if (this.#$.plugins[containerName].on) this.#$.plugins[containerName].on(button);
@@ -229,6 +232,7 @@ class Menu {
229
232
  dom.utils.removeClass(this.currentContainerActiveButton, 'on');
230
233
  this.currentContainerActiveButton = null;
231
234
  this.#$.ui.preventToolbarHide(false);
235
+ this.#context.get('menuTray').hidePopover?.();
232
236
  }
233
237
 
234
238
  this.#store.set('_preventBlur', false);