suneditor 3.1.4 → 3.2.1

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 (217) hide show
  1. package/dist/suneditor-contents.min.css +1 -1
  2. package/dist/suneditor.min.css +28 -1
  3. package/dist/suneditor.min.js +1 -1
  4. package/package.json +2 -1
  5. package/src/assets/design/color.css +12 -0
  6. package/src/assets/design/size.css +9 -2
  7. package/src/assets/icons/defaultIcons.js +96 -47
  8. package/src/assets/suneditor-contents.css +3 -2
  9. package/src/assets/suneditor.css +481 -31
  10. package/src/core/config/contextProvider.js +2 -1
  11. package/src/core/config/eventManager.js +41 -2
  12. package/src/core/config/optionProvider.js +23 -2
  13. package/src/core/editor.js +32 -14
  14. package/src/core/event/actions/index.js +76 -9
  15. package/src/core/event/effects/common.registry.js +2 -1
  16. package/src/core/event/effects/keydown.registry.js +169 -33
  17. package/src/core/event/effects/ruleHelpers.js +20 -6
  18. package/src/core/event/eventOrchestrator.js +162 -30
  19. package/src/core/event/handlers/handler_toolbar.js +4 -2
  20. package/src/core/event/handlers/handler_ww_dragDrop.js +10 -0
  21. package/src/core/event/handlers/handler_ww_input.js +84 -2
  22. package/src/core/event/handlers/handler_ww_key.js +82 -21
  23. package/src/core/event/handlers/handler_ww_mouse.js +45 -8
  24. package/src/core/event/ports.js +27 -13
  25. package/src/core/event/reducers/keydown.reducer.js +33 -5
  26. package/src/core/event/rules/keydown.rule.arrow.js +12 -2
  27. package/src/core/event/rules/keydown.rule.backspace.js +110 -15
  28. package/src/core/event/rules/keydown.rule.delete.js +62 -7
  29. package/src/core/event/rules/keydown.rule.enter.js +62 -31
  30. package/src/core/event/support/defaultLineManager.js +9 -2
  31. package/src/core/event/support/selectionState.js +13 -3
  32. package/src/core/kernel/coreKernel.js +1 -0
  33. package/src/core/logic/dom/char.js +13 -3
  34. package/src/core/logic/dom/format.js +171 -39
  35. package/src/core/logic/dom/html.js +350 -72
  36. package/src/core/logic/dom/inline.js +229 -34
  37. package/src/core/logic/dom/listFormat.js +111 -20
  38. package/src/core/logic/dom/nodeTransform.js +36 -11
  39. package/src/core/logic/dom/offset.js +116 -31
  40. package/src/core/logic/dom/selection.js +147 -29
  41. package/src/core/logic/panel/blockHandle.js +785 -0
  42. package/src/core/logic/panel/blockResolver.js +278 -0
  43. package/src/core/logic/panel/finder.js +16 -6
  44. package/src/core/logic/panel/menu.js +119 -18
  45. package/src/core/logic/panel/toolbar.js +75 -19
  46. package/src/core/logic/panel/viewer.js +76 -20
  47. package/src/core/logic/shell/_commandExecutor.js +44 -9
  48. package/src/core/logic/shell/commandDispatcher.js +46 -4
  49. package/src/core/logic/shell/component.js +102 -25
  50. package/src/core/logic/shell/focusManager.js +13 -3
  51. package/src/core/logic/shell/history.js +21 -6
  52. package/src/core/logic/shell/pluginManager.js +21 -13
  53. package/src/core/logic/shell/shortcuts.js +9 -1
  54. package/src/core/logic/shell/ui.js +139 -37
  55. package/src/core/schema/frameContext.js +34 -3
  56. package/src/core/schema/options.js +46 -4
  57. package/src/core/section/constructor.js +537 -106
  58. package/src/core/section/documentType.js +35 -6
  59. package/src/helper/converter.js +24 -7
  60. package/src/helper/dom/domCheck.js +25 -5
  61. package/src/helper/dom/domQuery.js +2 -1
  62. package/src/helper/dom/domUtils.js +17 -4
  63. package/src/helper/env.js +11 -2
  64. package/src/helper/keyCodeMap.js +6 -2
  65. package/src/helper/markdown.js +24 -5
  66. package/src/helper/msOffice.js +9 -2
  67. package/src/interfaces/plugins.js +1 -1
  68. package/src/langs/ckb.js +0 -1
  69. package/src/langs/cs.js +0 -1
  70. package/src/langs/da.js +0 -1
  71. package/src/langs/de.js +0 -1
  72. package/src/langs/en.js +0 -1
  73. package/src/langs/es.js +0 -1
  74. package/src/langs/fa.js +0 -1
  75. package/src/langs/fr.js +0 -1
  76. package/src/langs/he.js +0 -1
  77. package/src/langs/hu.js +0 -1
  78. package/src/langs/index.js +27 -1
  79. package/src/langs/it.js +0 -1
  80. package/src/langs/ja.js +0 -1
  81. package/src/langs/km.js +0 -1
  82. package/src/langs/ko.js +0 -1
  83. package/src/langs/lv.js +0 -1
  84. package/src/langs/nl.js +0 -1
  85. package/src/langs/pl.js +0 -1
  86. package/src/langs/pt_br.js +0 -1
  87. package/src/langs/ro.js +0 -1
  88. package/src/langs/ru.js +0 -1
  89. package/src/langs/se.js +0 -1
  90. package/src/langs/tr.js +0 -1
  91. package/src/langs/uk.js +0 -1
  92. package/src/langs/ur.js +0 -1
  93. package/src/langs/zh_cn.js +0 -1
  94. package/src/modules/contract/Browser.js +58 -14
  95. package/src/modules/contract/ColorPicker.js +12 -4
  96. package/src/modules/contract/Controller.js +37 -7
  97. package/src/modules/contract/Figure.js +156 -36
  98. package/src/modules/contract/HueSlider.js +27 -4
  99. package/src/modules/contract/Modal.js +50 -10
  100. package/src/modules/manager/FileManager.js +50 -8
  101. package/src/modules/ui/CommandMenu.js +597 -0
  102. package/src/modules/ui/ModalAnchorEditor.js +33 -6
  103. package/src/modules/ui/SelectMenu.js +481 -36
  104. package/src/modules/ui/index.js +1 -0
  105. package/src/plugins/browser/audioGallery.js +2 -1
  106. package/src/plugins/browser/fileBrowser.js +9 -2
  107. package/src/plugins/browser/fileGallery.js +2 -1
  108. package/src/plugins/browser/imageGallery.js +18 -3
  109. package/src/plugins/browser/videoGallery.js +13 -4
  110. package/src/plugins/command/blockquote.js +6 -1
  111. package/src/plugins/command/codeBlock.js +43 -5
  112. package/src/plugins/command/exportPDF.js +20 -4
  113. package/src/plugins/command/fileUpload.js +46 -11
  114. package/src/plugins/command/list_bulleted.js +6 -1
  115. package/src/plugins/command/list_numbered.js +6 -1
  116. package/src/plugins/dropdown/align.js +21 -33
  117. package/src/plugins/dropdown/backgroundColor.js +10 -2
  118. package/src/plugins/dropdown/blockStyle.js +17 -22
  119. package/src/plugins/dropdown/font.js +34 -34
  120. package/src/plugins/dropdown/hr.js +15 -43
  121. package/src/plugins/dropdown/layout.js +10 -27
  122. package/src/plugins/dropdown/lineHeight.js +10 -33
  123. package/src/plugins/dropdown/list.js +19 -25
  124. package/src/plugins/dropdown/paragraphStyle.js +16 -24
  125. package/src/plugins/dropdown/table/index.js +72 -17
  126. package/src/plugins/dropdown/table/render/table.html.js +3 -1
  127. package/src/plugins/dropdown/table/services/table.cell.js +33 -7
  128. package/src/plugins/dropdown/table/services/table.clipboard.js +5 -1
  129. package/src/plugins/dropdown/table/services/table.grid.js +40 -9
  130. package/src/plugins/dropdown/table/services/table.resize.js +55 -11
  131. package/src/plugins/dropdown/table/services/table.selection.js +27 -7
  132. package/src/plugins/dropdown/table/services/table.style.js +120 -20
  133. package/src/plugins/dropdown/template.js +13 -29
  134. package/src/plugins/dropdown/textStyle.js +22 -35
  135. package/src/plugins/field/autocomplete.js +8 -4
  136. package/src/plugins/field/slashCommand.js +297 -0
  137. package/src/plugins/index.js +3 -0
  138. package/src/plugins/input/fontSize.js +37 -8
  139. package/src/plugins/input/pageNavigator.js +7 -1
  140. package/src/plugins/modal/audio.js +71 -17
  141. package/src/plugins/modal/drawing.js +41 -11
  142. package/src/plugins/modal/embed.js +92 -22
  143. package/src/plugins/modal/image/index.js +114 -28
  144. package/src/plugins/modal/image/services/image.size.js +9 -2
  145. package/src/plugins/modal/image/services/image.upload.js +38 -4
  146. package/src/plugins/modal/link.js +9 -2
  147. package/src/plugins/modal/math.js +10 -2
  148. package/src/plugins/modal/video/index.js +112 -26
  149. package/src/plugins/modal/video/render/video.html.js +4 -1
  150. package/src/plugins/modal/video/services/video.size.js +15 -4
  151. package/src/plugins/modal/video/services/video.upload.js +10 -2
  152. package/src/plugins/popup/anchor.js +14 -2
  153. package/src/suneditor.js +8 -2
  154. package/src/themes/cobalt.css +12 -0
  155. package/src/themes/cream.css +12 -0
  156. package/src/themes/dark.css +12 -0
  157. package/src/themes/midnight.css +12 -0
  158. package/src/typedef.js +32 -21
  159. package/types/assets/icons/defaultIcons.d.ts +1 -0
  160. package/types/core/config/contextProvider.d.ts +7 -1
  161. package/types/core/config/eventManager.d.ts +37 -4
  162. package/types/core/config/optionProvider.d.ts +5 -1
  163. package/types/core/event/actions/index.d.ts +44 -6
  164. package/types/core/event/effects/keydown.registry.d.ts +37 -7
  165. package/types/core/event/effects/ruleHelpers.d.ts +6 -1
  166. package/types/core/event/eventOrchestrator.d.ts +13 -3
  167. package/types/core/event/handlers/handler_ww_clipboard.d.ts +15 -3
  168. package/types/core/event/handlers/handler_ww_dragDrop.d.ts +14 -2
  169. package/types/core/event/handlers/handler_ww_input.d.ts +10 -2
  170. package/types/core/event/handlers/handler_ww_key.d.ts +10 -2
  171. package/types/core/event/handlers/handler_ww_mouse.d.ts +25 -5
  172. package/types/core/event/ports.d.ts +11 -4
  173. package/types/core/event/reducers/keydown.reducer.d.ts +12 -3
  174. package/types/core/event/rules/keydown.rule.backspace.d.ts +6 -1
  175. package/types/core/kernel/store.d.ts +4 -1
  176. package/types/core/logic/dom/format.d.ts +32 -14
  177. package/types/core/logic/dom/html.d.ts +11 -3
  178. package/types/core/logic/dom/nodeTransform.d.ts +6 -6
  179. package/types/core/logic/panel/blockHandle.d.ts +64 -0
  180. package/types/core/logic/panel/blockResolver.d.ts +50 -0
  181. package/types/core/logic/panel/menu.d.ts +72 -3
  182. package/types/core/logic/shell/commandDispatcher.d.ts +8 -0
  183. package/types/core/logic/shell/component.d.ts +5 -3
  184. package/types/core/logic/shell/shortcuts.d.ts +10 -1
  185. package/types/core/logic/shell/ui.d.ts +7 -1
  186. package/types/core/schema/context.d.ts +7 -1
  187. package/types/core/schema/frameContext.d.ts +36 -4
  188. package/types/core/schema/options.d.ts +83 -2
  189. package/types/core/section/constructor.d.ts +38 -1
  190. package/types/events.d.ts +208 -32
  191. package/types/helper/converter.d.ts +4 -2
  192. package/types/helper/dom/domCheck.d.ts +12 -2
  193. package/types/helper/dom/domQuery.d.ts +35 -7
  194. package/types/helper/dom/domUtils.d.ts +4 -1
  195. package/types/interfaces/plugins.d.ts +2 -2
  196. package/types/langs/_Lang.d.ts +0 -1
  197. package/types/modules/contract/ColorPicker.d.ts +2 -2
  198. package/types/modules/contract/Controller.d.ts +7 -0
  199. package/types/modules/contract/Figure.d.ts +31 -4
  200. package/types/modules/ui/CommandMenu.d.ts +262 -0
  201. package/types/modules/ui/SelectMenu.d.ts +56 -3
  202. package/types/modules/ui/index.d.ts +1 -0
  203. package/types/plugins/browser/audioGallery.d.ts +1 -1
  204. package/types/plugins/browser/fileBrowser.d.ts +1 -1
  205. package/types/plugins/browser/fileGallery.d.ts +1 -1
  206. package/types/plugins/browser/imageGallery.d.ts +1 -1
  207. package/types/plugins/browser/videoGallery.d.ts +1 -1
  208. package/types/plugins/dropdown/align.d.ts +1 -1
  209. package/types/plugins/dropdown/blockStyle.d.ts +3 -1
  210. package/types/plugins/dropdown/table/index.d.ts +4 -1
  211. package/types/plugins/dropdown/table/services/table.clipboard.d.ts +5 -1
  212. package/types/plugins/dropdown/table/services/table.grid.d.ts +10 -2
  213. package/types/plugins/field/autocomplete.d.ts +0 -1
  214. package/types/plugins/field/slashCommand.d.ts +165 -0
  215. package/types/plugins/index.d.ts +3 -0
  216. package/types/plugins/modal/link.d.ts +4 -1
  217. package/types/typedef.d.ts +48 -23
@@ -20,7 +20,12 @@ export default {
20
20
  if (rInfo.commonCon !== rInfo.container && formatEl.parentElement) {
21
21
  if (formatEl.contains(container)) {
22
22
  const focusNode = LineDelete_next(formatEl);
23
- ports.selection.setRange(focusNode, focusNode.textContent.length, focusNode, focusNode.textContent.length);
23
+ ports.selection.setRange(
24
+ focusNode,
25
+ focusNode.textContent.length,
26
+ focusNode,
27
+ focusNode.textContent.length,
28
+ );
24
29
  } else {
25
30
  const { focusNode, focusOffset } = LineDelete_prev(formatEl);
26
31
  ports.selection.setRange(focusNode, focusOffset, focusNode, focusOffset);
@@ -64,7 +69,10 @@ export default {
64
69
  formatEl.removeAttribute(attrs[0].name);
65
70
  }
66
71
  } else {
67
- formatEl.parentNode.replaceChild(dom.utils.createElement(ctx.options.get('defaultLine'), null, '<br>'), formatEl);
72
+ formatEl.parentNode.replaceChild(
73
+ dom.utils.createElement(ctx.options.get('defaultLine'), null, '<br>'),
74
+ formatEl,
75
+ );
68
76
  }
69
77
  },
70
78
 
@@ -72,16 +80,19 @@ export default {
72
80
  'backspace.component.select': ({ ports }, { selectionNode, range, fileComponentInfo }) => {
73
81
  let currentZWS = null;
74
82
  if (dom.check.isBreak(selectionNode)) dom.utils.removeItem(selectionNode);
75
- else if (dom.check.isBreak((currentZWS = range.startContainer.childNodes?.[range.startOffset]))) dom.utils.removeItem(currentZWS);
83
+ else if (dom.check.isBreak((currentZWS = range.startContainer.childNodes?.[range.startOffset])))
84
+ dom.utils.removeItem(currentZWS);
76
85
 
77
- if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
86
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false)
87
+ ports.focusManager.blur();
78
88
  },
79
89
 
80
90
  /** @action backspaceComponentRemove */
81
91
  'backspace.component.remove': ({ ports }, { isList, sel, formatEl, fileComponentInfo }) => {
82
92
  if (isList) dom.utils.removeItem(sel);
83
93
  if (formatEl.textContent.length === 0) dom.utils.removeItem(formatEl);
84
- if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
94
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false)
95
+ ports.focusManager.blur();
85
96
  },
86
97
 
87
98
  /** @action backspaceListMergePrev */
@@ -110,7 +121,37 @@ export default {
110
121
  'backspace.list.removeNested': ({ ports }, { range }) => {
111
122
  ports.html.remove();
112
123
  if (range.startContainer.nodeType === 3) {
113
- ports.selection.setRange(range.startContainer, range.startContainer.textContent.length, range.startContainer, range.startContainer.textContent.length);
124
+ ports.selection.setRange(
125
+ range.startContainer,
126
+ range.startContainer.textContent.length,
127
+ range.startContainer,
128
+ range.startContainer.textContent.length,
129
+ );
130
+ }
131
+ },
132
+
133
+ /** @action backspaceEmptyLineMergePrev */
134
+ 'backspace.emptyLine.mergePrev': ({ ports }, { formatEl, prev }) => {
135
+ dom.utils.removeItem(formatEl);
136
+ caretToLineEdge(ports, prev, true);
137
+ },
138
+
139
+ /** @action backspaceBrLineRowMerge */
140
+ 'backspace.brline.rowMerge': ({ ports }, { rowEndBr, rowStartBr }) => {
141
+ const between = rowStartBr.nextSibling;
142
+ if (between !== rowEndBr && between?.nodeType === 3 && dom.check.isZeroWidth(between)) {
143
+ dom.utils.removeItem(between);
144
+ }
145
+ const anchor = rowStartBr.previousSibling;
146
+ dom.utils.removeItem(rowStartBr);
147
+
148
+ if (anchor && anchor.nodeType === 3) {
149
+ ports.selection.setRange(anchor, anchor.textContent.length, anchor, anchor.textContent.length);
150
+ } else if (dom.check.isBreak(anchor)) {
151
+ ports.selection.setRange(rowEndBr, 0, rowEndBr, 0);
152
+ } else {
153
+ const pre = rowEndBr.parentNode;
154
+ ports.selection.setRange(pre, 0, pre, 0);
114
155
  }
115
156
  },
116
157
 
@@ -125,7 +166,8 @@ export default {
125
166
  dom.utils.removeItem(formatEl);
126
167
  }
127
168
 
128
- if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
169
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false)
170
+ ports.focusManager.blur();
129
171
  },
130
172
 
131
173
  /** @action deleteComponentSelectNext */
@@ -146,7 +188,8 @@ export default {
146
188
  const fileComponentInfo = ports.component.get(nextEl);
147
189
  if (fileComponentInfo) {
148
190
  ctx.e.stopPropagation();
149
- if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.focusManager.blur();
191
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false)
192
+ ports.focusManager.blur();
150
193
  } else if (ports.component.is(nextEl)) {
151
194
  ctx.e.stopPropagation();
152
195
  dom.utils.removeItem(nextEl);
@@ -157,7 +200,11 @@ export default {
157
200
  'delete.list.removeNested': ({ ports, ctx }, { range, formatEl, rangeEl }) => {
158
201
  if (range.startContainer !== range.endContainer) ports.html.remove();
159
202
 
160
- const next = /** @type {HTMLElement} */ (dom.utils.arrayFind(formatEl.children, dom.check.isList) || formatEl.nextElementSibling || rangeEl.parentElement.nextElementSibling);
203
+ const next = /** @type {HTMLElement} */ (
204
+ dom.utils.arrayFind(formatEl.children, dom.check.isList) ||
205
+ formatEl.nextElementSibling ||
206
+ rangeEl.parentElement.nextElementSibling
207
+ );
161
208
  if (next && (dom.check.isList(next) || dom.utils.arrayFind(next.children, dom.check.isList))) {
162
209
  ctx.e.preventDefault();
163
210
 
@@ -184,6 +231,27 @@ export default {
184
231
  }
185
232
  },
186
233
 
234
+ /** @action deleteEmptyLineMergeNext — remove an empty line, move caret to the start of the next line */
235
+ 'delete.emptyLine.mergeNext': ({ ports }, { formatEl, next }) => {
236
+ dom.utils.removeItem(formatEl);
237
+ caretToLineEdge(ports, next, false);
238
+ },
239
+
240
+ /** @action deleteBrLineRowMerge — remove an empty row inside a brLine (PRE), pull the next row up */
241
+ 'delete.brline.rowMerge': ({ ports }, { rowEndBr }) => {
242
+ let next = rowEndBr.nextSibling;
243
+ if (next?.nodeType === 3 && dom.check.isZeroWidth(next)) next = next.nextSibling; // skip a trailing zero-width
244
+ const pre = rowEndBr.parentNode;
245
+ dom.utils.removeItem(rowEndBr);
246
+
247
+ // caret → start of the pulled-up row
248
+ if (next && (next.nodeType === 3 || dom.check.isBreak(next))) {
249
+ ports.selection.setRange(next, 0, next, 0);
250
+ } else {
251
+ ports.selection.setRange(pre, pre.childNodes.length, pre, pre.childNodes.length);
252
+ }
253
+ },
254
+
187
255
  /** [tab] */
188
256
 
189
257
  /** @action tabFormatIndent */
@@ -224,8 +292,15 @@ export default {
224
292
  if (lines.length === 1) {
225
293
  let tabSize = ctx.store.get('tabSize') + 1;
226
294
  if (ctx.options.get('syncTabIndent')) {
227
- const baseIndex = dom.query.findTextIndexOnLine(formatEl, range.startContainer, range.startOffset, (current) => ports.component.is(current));
228
- const prevTabEndIndex = ports.format.isLine(formatEl.previousElementSibling) ? dom.query.findTabEndIndex(formatEl.previousElementSibling, baseIndex, 2) : 0;
295
+ const baseIndex = dom.query.findTextIndexOnLine(
296
+ formatEl,
297
+ range.startContainer,
298
+ range.startOffset,
299
+ (current) => ports.component.is(current),
300
+ );
301
+ const prevTabEndIndex = ports.format.isLine(formatEl.previousElementSibling)
302
+ ? dom.query.findTabEndIndex(formatEl.previousElementSibling, baseIndex, 2)
303
+ : 0;
229
304
  if (prevTabEndIndex > baseIndex) {
230
305
  tabSize = prevTabEndIndex - baseIndex;
231
306
  }
@@ -309,16 +384,10 @@ export default {
309
384
  },
310
385
 
311
386
  /** @action enterLineAddDefault */
312
- 'enter.line.addDefault': ({ ports, ctx }, { formatEl }) => {
313
- const newFormat = ports.format.addLine(formatEl, ctx.options.get('defaultLine'));
387
+ 'enter.line.addDefault': ({ ports }, { formatEl }) => {
388
+ const newFormat = ports.format.addLineAfter(formatEl);
314
389
  const temp = newFormat.firstChild;
315
- if (dom.check.isBreak(temp)) {
316
- const zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
317
- temp.parentNode.insertBefore(zeroWidth, temp);
318
- ports.selection.setRange(zeroWidth, 1, zeroWidth, 1);
319
- } else {
320
- ports.selection.setRange(temp, 0, temp, 0);
321
- }
390
+ ports.selection.setRange(temp, 0, temp, 0);
322
391
  },
323
392
 
324
393
  /** @action enterListAddItem */
@@ -359,7 +428,12 @@ export default {
359
428
  }
360
429
 
361
430
  newEl = dom.utils.createElement(newFormat);
362
- const edge = ports.format.removeBlock(rangeEl, { selectedFormats: [formatEl], newBlockElement: null, shouldDelete: true, skipHistory: true });
431
+ const edge = ports.format.removeBlock(rangeEl, {
432
+ selectedFormats: [formatEl],
433
+ newBlockElement: null,
434
+ shouldDelete: true,
435
+ skipHistory: true,
436
+ });
363
437
  edge.cc.insertBefore(newEl, edge.ec);
364
438
  }
365
439
 
@@ -380,7 +454,12 @@ export default {
380
454
 
381
455
  /** @action enterFormatInsertBrHtml */
382
456
  'enter.format.insertBrHtml': ({ ports }, { brBlock, range, wSelection, offset }) => {
383
- ports.html.insert(range.collapsed && dom.check.isBreak(range.startContainer.childNodes[range.startOffset - 1]) ? '<br>' : '<br><br>', { selectInserted: false, skipCharCount: true, skipCleaning: true });
457
+ ports.html.insert(
458
+ range.collapsed && dom.check.isBreak(range.startContainer.childNodes[range.startOffset - 1])
459
+ ? '<br>'
460
+ : '<br><br>',
461
+ { selectInserted: false, skipCharCount: true, skipCleaning: true },
462
+ );
384
463
 
385
464
  let focusNode = wSelection.focusNode;
386
465
  const wOffset = wSelection.focusOffset;
@@ -392,6 +471,34 @@ export default {
392
471
  ports.setOnShortcutKey(true);
393
472
  },
394
473
 
474
+ /** @action enterBrLineInsert — insert exactly one empty row at the caret inside a normal brLine. */
475
+ 'enter.brline.insert': ({ ports }, { range }) => {
476
+ // Restore the caret captured at rule time (enterPrevent may have moved the live selection).
477
+ ports.selection.setRange(range.startContainer, range.startOffset, range.endContainer, range.endOffset);
478
+ if (!range.collapsed) ports.html.remove();
479
+
480
+ const br = dom.utils.createElement('BR');
481
+ ports.html.insertNode(br, { afterNode: null, skipCharCount: true });
482
+
483
+ if (!br.nextSibling) br.parentNode.appendChild(dom.utils.createElement('BR'));
484
+ const lower = br.nextSibling;
485
+ ports.selection.setRange(lower, 0, lower, 0);
486
+ ports.setOnShortcutKey(true);
487
+ },
488
+
489
+ /** @action enterBrLineExit — consume only the caret's current (last) empty row and add a default line after the brLine. */
490
+ 'enter.brline.exit': ({ ports }, { brBlock }) => {
491
+ const last = brBlock.lastChild;
492
+ if (last && (dom.check.isBreak(last) || dom.check.isZeroWidth(last.textContent))) {
493
+ dom.utils.removeItem(last);
494
+ }
495
+
496
+ const brBlockNext = /** @type {HTMLElement} */ (brBlock).nextElementSibling;
497
+ const newEl = ports.format.addLine(brBlock, ports.format.isLine(brBlockNext) ? brBlockNext : null);
498
+ dom.utils.copyFormatAttributes(newEl, brBlock);
499
+ ports.selection.setRange(newEl, 1, newEl, 1);
500
+ },
501
+
395
502
  /** @action enterFormatInsertBrNode */
396
503
  'enter.format.insertBrNode': ({ ports }, { wSelection }) => {
397
504
  const focusNext = wSelection.focusNode.nextSibling;
@@ -411,7 +518,10 @@ export default {
411
518
  },
412
519
 
413
520
  /** @action enterFormatBreakAtEdge */
414
- 'enter.format.breakAtEdge': ({ ports, ctx }, { formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped }) => {
521
+ 'enter.format.breakAtEdge': (
522
+ { ports, ctx },
523
+ { formatEl, selectionNode, formatStartEdge, formatEndEdge, bidiSwapped },
524
+ ) => {
415
525
  const focusBR = dom.utils.createElement('BR');
416
526
  const newFormat = dom.utils.createElement(formatEl.nodeName, null, focusBR);
417
527
 
@@ -429,7 +539,10 @@ export default {
429
539
  } while (formatEl !== sNode && formatEl.contains(sNode));
430
540
 
431
541
  newFormat.appendChild(child);
432
- formatEl.parentNode.insertBefore(newFormat, formatStartEdge && !formatEndEdge ? formatEl : formatEl.nextElementSibling);
542
+ formatEl.parentNode.insertBefore(
543
+ newFormat,
544
+ formatStartEdge && !formatEndEdge ? formatEl : formatEl.nextElementSibling,
545
+ );
433
546
  if (formatEndEdge) {
434
547
  ports.selection.setRange(focusBR, 1, focusBR, 1);
435
548
  } else if (bidiSwapped) {
@@ -442,13 +555,19 @@ export default {
442
555
 
443
556
  /** @action enterFormatBreakWithSelection */
444
557
  'enter.format.breakWithSelection': ({ ports, ctx }, { formatEl, range, formatStartEdge, formatEndEdge }) => {
445
- const isMultiLine = ports.format.getLine(range.startContainer, null) !== ports.format.getLine(range.endContainer, null);
558
+ const isMultiLine =
559
+ ports.format.getLine(range.startContainer, null) !== ports.format.getLine(range.endContainer, null);
446
560
  const newFormat = /** @type {HTMLElement} */ (formatEl.cloneNode(false));
447
561
  newFormat.innerHTML = '<br>';
448
562
  const commonCon = /** @type {HTMLElement} */ (range.commonAncestorContainer);
449
563
  const rcon =
450
564
  commonCon === range.startContainer && commonCon === range.endContainer && dom.check.isZeroWidth(commonCon)
451
- ? { container: commonCon, offset: range.endOffset, prevContainer: commonCon.previousElementSibling, commonCon: commonCon }
565
+ ? {
566
+ container: commonCon,
567
+ offset: range.endOffset,
568
+ prevContainer: commonCon.previousElementSibling,
569
+ commonCon: commonCon,
570
+ }
452
571
  : ports.html.remove();
453
572
 
454
573
  let newEl = ports.format.getLine(rcon.container, null);
@@ -467,26 +586,29 @@ export default {
467
586
  }
468
587
 
469
588
  const innerRange = ports.format.getBlock(rcon.container);
470
- newEl = newEl.contains(innerRange) ? dom.query.getEdgeChild(innerRange, (current) => Boolean(ports.format.getLine(current)), false) : newEl;
589
+ newEl = newEl.contains(innerRange)
590
+ ? dom.query.getEdgeChild(innerRange, (current) => Boolean(ports.format.getLine(current)), false)
591
+ : newEl;
471
592
  if (isMultiLine) {
472
593
  if (formatEndEdge && !formatStartEdge) {
473
- newEl.parentNode.insertBefore(newFormat, !rcon.prevContainer || rcon.container === rcon.prevContainer ? newEl.nextElementSibling : newEl);
594
+ newEl.parentNode.insertBefore(
595
+ newFormat,
596
+ !rcon.prevContainer || rcon.container === rcon.prevContainer ? newEl.nextElementSibling : newEl,
597
+ );
474
598
  newEl = newFormat;
475
599
  offset = 0;
476
600
  } else {
477
601
  offset = rcon.offset;
478
602
  if (formatStartEdge) {
479
- const tempEl = newEl.parentNode.insertBefore(newFormat, newEl);
603
+ newEl.parentNode.insertBefore(newFormat, newEl);
480
604
  if (formatEndEdge) {
481
- newEl = tempEl;
482
605
  offset = 0;
483
606
  }
484
607
  }
485
608
  }
486
609
  } else {
487
610
  if (formatEndEdge && formatStartEdge) {
488
- newEl.parentNode.insertBefore(newFormat, rcon.prevContainer && rcon.container === rcon.prevContainer ? newEl.nextElementSibling : newEl);
489
- newEl = newFormat;
611
+ newEl.parentNode.insertBefore(newFormat, newEl);
490
612
  offset = 0;
491
613
  } else if (formatEndEdge) {
492
614
  newEl = newEl.parentNode.insertBefore(newFormat, newEl.nextElementSibling);
@@ -526,7 +648,10 @@ export default {
526
648
  /** [keydown reducer] */
527
649
  /** @action keydownInputInsertNbsp */
528
650
  'keydown.input.insertNbsp': ({ ports }) => {
529
- const nbsp = ports.html.insertNode(dom.utils.createTextNode('\u00a0'), { afterNode: null, skipCharCount: true });
651
+ const nbsp = ports.html.insertNode(dom.utils.createTextNode('\u00a0'), {
652
+ afterNode: null,
653
+ skipCharCount: true,
654
+ });
530
655
  if (nbsp) {
531
656
  ports.selection.setRange(nbsp, nbsp.length, nbsp, nbsp.length);
532
657
  }
@@ -540,6 +665,17 @@ export default {
540
665
  },
541
666
  };
542
667
 
668
+ /**
669
+ * Place a collapsed caret at the start or end of a line after an empty line is removed.
670
+ * @param {import('../ports').EventReducerPorts} ports - Ports for interacting with editor
671
+ * @param {Element} line - The destination line element
672
+ * @param {boolean} toEnd - true → caret at the line end (backspace), false → line start (delete)
673
+ */
674
+ function caretToLineEdge(ports, line, toEnd) {
675
+ const offset = toEnd ? line.childNodes.length : 0;
676
+ ports.selection.setRange(line, offset, line, offset);
677
+ }
678
+
543
679
  /**
544
680
  * @param {HTMLElement} formatEl - Format element
545
681
  * @returns {Node}
@@ -35,8 +35,14 @@ function hardDelete(ports) {
35
35
  const eCell = ports.format.getBlock(ec);
36
36
  const sIsCell = dom.check.isTableCell(sCell);
37
37
  const eIsCell = dom.check.isTableCell(eCell);
38
- if (((sIsCell && !sCell.previousElementSibling && !sCell.parentElement.previousElementSibling) || (eIsCell && !eCell.nextElementSibling && !eCell.parentElement.nextElementSibling)) && sCell !== eCell) {
39
- const ancestor = dom.query.getParentElement(range.commonAncestorContainer, dom.check.isFigure)?.parentElement || range.commonAncestorContainer;
38
+ if (
39
+ ((sIsCell && !sCell.previousElementSibling && !sCell.parentElement.previousElementSibling) ||
40
+ (eIsCell && !eCell.nextElementSibling && !eCell.parentElement.nextElementSibling)) &&
41
+ sCell !== eCell
42
+ ) {
43
+ const ancestor =
44
+ dom.query.getParentElement(range.commonAncestorContainer, dom.check.isFigure)?.parentElement ||
45
+ range.commonAncestorContainer;
40
46
  if (!sIsCell) {
41
47
  dom.utils.removeItem(dom.query.getParentElement(eCell, (current) => ancestor === current.parentNode));
42
48
  } else if (!eIsCell) {
@@ -104,11 +110,18 @@ function isUneditableNode(ports, range, isFront) {
104
110
 
105
111
  let siblingNode;
106
112
  if (isElement) {
107
- siblingNode = /** @type {HTMLElement} */ (_isUneditableNode_getSibling(ports, container.childNodes[offset], siblingKey, container));
113
+ siblingNode = /** @type {HTMLElement} */ (
114
+ _isUneditableNode_getSibling(ports, container.childNodes[offset], siblingKey, container)
115
+ );
108
116
  return dom.check.isComponentContainer(siblingNode) || dom.check.isNonEditable(siblingNode) ? siblingNode : null;
109
117
  } else {
110
- siblingNode = /** @type {HTMLElement} */ (_isUneditableNode_getSibling(ports, container, siblingKey, container));
111
- return dom.check.isEdgePoint(container, offset, isFront ? 'front' : 'end') && (dom.check.isComponentContainer(siblingNode) || dom.check.isNonEditable(siblingNode)) ? siblingNode : null;
118
+ siblingNode = /** @type {HTMLElement} */ (
119
+ _isUneditableNode_getSibling(ports, container, siblingKey, container)
120
+ );
121
+ return dom.check.isEdgePoint(container, offset, isFront ? 'front' : 'end') &&
122
+ (dom.check.isComponentContainer(siblingNode) || dom.check.isNonEditable(siblingNode))
123
+ ? siblingNode
124
+ : null;
112
125
  }
113
126
  }
114
127
 
@@ -128,7 +141,8 @@ function _isUneditableNode_getSibling(ports, selectNode, siblingKey, container)
128
141
  if (!siblingNode) {
129
142
  siblingNode = ports.format.getLine(container);
130
143
  siblingNode = siblingNode ? siblingNode[siblingKey] : null;
131
- if (siblingNode && !ports.component.is(siblingNode)) siblingNode = siblingKey === 'previousSibling' ? siblingNode.firstChild : siblingNode.lastChild;
144
+ if (siblingNode && !ports.component.is(siblingNode))
145
+ siblingNode = siblingKey === 'previousSibling' ? siblingNode.firstChild : siblingNode.lastChild;
132
146
  else return null;
133
147
  }
134
148