suneditor 3.0.0-beta.26 → 3.0.0-beta.28

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 (135) hide show
  1. package/dist/suneditor.min.css +1 -1
  2. package/dist/suneditor.min.js +1 -1
  3. package/package.json +77 -39
  4. package/src/core/{section → base}/actives.js +1 -1
  5. package/src/core/class/component.js +2 -0
  6. package/src/core/class/format.js +44 -2435
  7. package/src/core/class/html.js +5 -4
  8. package/src/core/class/inline.js +1853 -0
  9. package/src/core/class/listFormat.js +582 -0
  10. package/src/core/class/nodeTransform.js +1 -3
  11. package/src/core/class/selection.js +4 -2
  12. package/src/core/class/ui.js +1 -1
  13. package/src/core/class/viewer.js +4 -4
  14. package/src/core/config/options.js +37 -18
  15. package/src/core/editor.js +43 -29
  16. package/src/core/event/actions/index.js +229 -0
  17. package/src/core/event/effects/common.registry.js +60 -0
  18. package/src/core/event/effects/keydown.registry.js +551 -0
  19. package/src/core/event/effects/ruleHelpers.js +145 -0
  20. package/src/core/{base → event}/eventManager.js +8 -124
  21. package/src/core/event/executor.js +21 -0
  22. package/src/core/{base/eventHandlers → event/handlers}/handler_toolbar.js +1 -1
  23. package/src/core/event/handlers/handler_ww_input.js +77 -0
  24. package/src/core/event/handlers/handler_ww_key.js +228 -0
  25. package/src/core/{base/eventHandlers → event/handlers}/handler_ww_mouse.js +3 -3
  26. package/src/core/event/ports.js +211 -0
  27. package/src/core/event/reducers/keydown.reducer.js +89 -0
  28. package/src/core/event/rules/keydown.rule.arrow.js +54 -0
  29. package/src/core/event/rules/keydown.rule.backspace.js +202 -0
  30. package/src/core/event/rules/keydown.rule.delete.js +126 -0
  31. package/src/core/event/rules/keydown.rule.enter.js +144 -0
  32. package/src/core/event/rules/keydown.rule.tab.js +29 -0
  33. package/src/core/section/constructor.js +57 -23
  34. package/src/editorInjector/_classes.js +4 -0
  35. package/src/editorInjector/index.js +4 -0
  36. package/src/helper/clipboard.js +0 -1
  37. package/src/helper/converter.js +6 -7
  38. package/src/helper/dom/domCheck.js +1 -1
  39. package/src/helper/dom/domQuery.js +1 -1
  40. package/src/helper/dom/domUtils.js +2 -2
  41. package/src/helper/dom/index.js +4 -0
  42. package/src/helper/env.js +1 -6
  43. package/src/helper/keyCodeMap.js +0 -1
  44. package/src/langs/ckb.js +1 -1
  45. package/src/langs/cs.js +1 -1
  46. package/src/langs/da.js +1 -1
  47. package/src/langs/de.js +1 -1
  48. package/src/langs/en.js +1 -1
  49. package/src/langs/es.js +1 -1
  50. package/src/langs/fa.js +1 -1
  51. package/src/langs/fr.js +1 -1
  52. package/src/langs/he.js +1 -1
  53. package/src/langs/hu.js +1 -1
  54. package/src/langs/it.js +1 -1
  55. package/src/langs/ja.js +1 -1
  56. package/src/langs/km.js +1 -1
  57. package/src/langs/ko.js +1 -1
  58. package/src/langs/lv.js +1 -1
  59. package/src/langs/nl.js +1 -1
  60. package/src/langs/pl.js +1 -1
  61. package/src/langs/pt_br.js +1 -1
  62. package/src/langs/ro.js +1 -1
  63. package/src/langs/ru.js +1 -1
  64. package/src/langs/se.js +1 -1
  65. package/src/langs/tr.js +1 -1
  66. package/src/langs/uk.js +1 -1
  67. package/src/langs/ur.js +1 -1
  68. package/src/langs/zh_cn.js +1 -1
  69. package/src/modules/ApiManager.js +5 -0
  70. package/src/modules/Figure.js +4 -10
  71. package/src/modules/HueSlider.js +18 -4
  72. package/src/modules/SelectMenu.js +1 -1
  73. package/src/plugins/command/fileUpload.js +1 -1
  74. package/src/plugins/command/list_bulleted.js +1 -1
  75. package/src/plugins/command/list_numbered.js +1 -1
  76. package/src/plugins/dropdown/backgroundColor.js +2 -2
  77. package/src/plugins/dropdown/font.js +2 -2
  78. package/src/plugins/dropdown/fontColor.js +2 -2
  79. package/src/plugins/dropdown/list.js +1 -1
  80. package/src/plugins/dropdown/table.js +1 -3
  81. package/src/plugins/dropdown/textStyle.js +1 -1
  82. package/src/plugins/field/mention.js +2 -2
  83. package/src/plugins/input/fontSize.js +9 -9
  84. package/src/plugins/modal/audio.js +5 -5
  85. package/src/plugins/modal/embed.js +5 -5
  86. package/src/plugins/modal/image.js +7 -7
  87. package/src/plugins/modal/link.js +23 -8
  88. package/src/plugins/modal/video.js +5 -5
  89. package/src/suneditor.js +9 -34
  90. package/src/typedef.js +15 -9
  91. package/types/core/class/format.d.ts +2 -352
  92. package/types/core/class/html.d.ts +2 -2
  93. package/types/core/class/inline.d.ts +263 -0
  94. package/types/core/class/listFormat.d.ts +135 -0
  95. package/types/core/config/options.d.ts +52 -78
  96. package/types/core/editor.d.ts +22 -12
  97. package/types/core/event/actions/index.d.ts +47 -0
  98. package/types/core/event/effects/common.registry.d.ts +50 -0
  99. package/types/core/event/effects/keydown.registry.d.ts +73 -0
  100. package/types/core/event/effects/ruleHelpers.d.ts +31 -0
  101. package/types/core/{base → event}/eventManager.d.ts +0 -42
  102. package/types/core/event/executor.d.ts +6 -0
  103. package/types/core/event/handlers/handler_ww_input.d.ts +41 -0
  104. package/types/core/{base/eventHandlers/handler_ww_key_input.d.ts → event/handlers/handler_ww_key.d.ts} +4 -33
  105. package/types/core/event/ports.d.ts +255 -0
  106. package/types/core/event/reducers/keydown.reducer.d.ts +75 -0
  107. package/types/core/event/rules/keydown.rule.arrow.d.ts +8 -0
  108. package/types/core/event/rules/keydown.rule.backspace.d.ts +9 -0
  109. package/types/core/event/rules/keydown.rule.delete.d.ts +9 -0
  110. package/types/core/event/rules/keydown.rule.enter.d.ts +9 -0
  111. package/types/core/event/rules/keydown.rule.tab.d.ts +9 -0
  112. package/types/core/section/constructor.d.ts +165 -39
  113. package/types/editorInjector/_classes.d.ts +4 -0
  114. package/types/editorInjector/index.d.ts +4 -0
  115. package/types/helper/converter.d.ts +4 -20
  116. package/types/helper/dom/index.d.ts +86 -1
  117. package/types/index.d.ts +11 -121
  118. package/types/langs/index.d.ts +2 -2
  119. package/types/modules/HueSlider.d.ts +12 -0
  120. package/types/modules/index.d.ts +3 -3
  121. package/types/plugins/index.d.ts +38 -38
  122. package/types/plugins/modal/link.d.ts +6 -4
  123. package/types/suneditor.d.ts +18 -19
  124. package/types/typedef.d.ts +6 -2
  125. package/src/core/base/eventHandlers/handler_ww_key_input.js +0 -1267
  126. package/types/core/section/context.d.ts +0 -67
  127. package/types/core/section/options.d.ts +0 -1022
  128. package/types/langs/_Lang.d.ts +0 -194
  129. /package/src/core/{base/eventHandlers → event/handlers}/handler_ww_clipboard.js +0 -0
  130. /package/src/core/{base/eventHandlers → event/handlers}/handler_ww_dragDrop.js +0 -0
  131. /package/types/core/{section → base}/actives.d.ts +0 -0
  132. /package/types/core/{base/eventHandlers → event/handlers}/handler_toolbar.d.ts +0 -0
  133. /package/types/core/{base/eventHandlers → event/handlers}/handler_ww_clipboard.d.ts +0 -0
  134. /package/types/core/{base/eventHandlers → event/handlers}/handler_ww_dragDrop.d.ts +0 -0
  135. /package/types/core/{base/eventHandlers → event/handlers}/handler_ww_mouse.d.ts +0 -0
@@ -0,0 +1,551 @@
1
+ import { dom, unicode } from '../../../helper';
2
+
3
+ /**
4
+ * @typedef {Object} EffectContext_keydown
5
+ * @property {__se__EventPorts} ports - Ports for interacting with editor
6
+ * @property {__se__EventKeydownCtx} ctx - Reducer context
7
+ */
8
+
9
+ /**
10
+ * @typedef {(ctx: EffectContext_keydown, payload?: *) => *} Effect
11
+ */
12
+
13
+ /** @type {Record<string, Effect>} */
14
+ export default {
15
+ // backspace and delete
16
+ /** @action delFormatRemoveAndMove */
17
+ 'del.format.removeAndMove': ({ ports }, { container, formatEl }) => {
18
+ const rInfo = ports.html.remove();
19
+ if (rInfo.commonCon !== rInfo.container && formatEl.parentElement) {
20
+ if (formatEl.contains(container)) {
21
+ const focusNode = LineDelete_next(formatEl);
22
+ ports.selection.setRange(focusNode, focusNode.textContent.length, focusNode, focusNode.textContent.length);
23
+ } else {
24
+ const { focusNode, focusOffset } = LineDelete_prev(formatEl);
25
+ ports.selection.setRange(focusNode, focusOffset, focusNode, focusOffset);
26
+ }
27
+ }
28
+ },
29
+
30
+ // backspace
31
+ /** @action backspaceFormatMaintain */
32
+ 'backspace.format.maintain': ({ ctx }, { formatEl }) => {
33
+ if (formatEl.nodeName.toUpperCase() === ctx.options.get('defaultLine').toUpperCase()) {
34
+ formatEl.innerHTML = '<br>';
35
+ const attrs = formatEl.attributes;
36
+ while (attrs[0]) {
37
+ formatEl.removeAttribute(attrs[0].name);
38
+ }
39
+ } else {
40
+ formatEl.parentNode.replaceChild(dom.utils.createElement(ctx.options.get('defaultLine'), null, '<br>'), formatEl);
41
+ }
42
+ },
43
+ /** @action backspaceComponentSelect */
44
+ 'backspace.component.select': ({ ports }, { selectionNode, range, fileComponentInfo }) => {
45
+ let currentZWS = null;
46
+ if (dom.check.isBreak(selectionNode)) dom.utils.removeItem(selectionNode);
47
+ else if (dom.check.isBreak((currentZWS = range.startContainer.childNodes?.[range.startOffset]))) dom.utils.removeItem(currentZWS);
48
+
49
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.editor.blur();
50
+ },
51
+ /** @action backspaceComponentRemove */
52
+ 'backspace.component.remove': ({ ports }, { isList, sel, formatEl, fileComponentInfo }) => {
53
+ if (isList) dom.utils.removeItem(sel);
54
+ if (formatEl.textContent.length === 0) dom.utils.removeItem(formatEl);
55
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.editor.blur();
56
+ },
57
+ /** @action backspaceListMergePrev */
58
+ 'backspace.list.mergePrev': ({ ports }, { prev, formatEl, rangeEl }) => {
59
+ let con = prev === rangeEl.parentNode ? rangeEl.previousSibling : prev.lastChild;
60
+ if (!con) {
61
+ con = dom.utils.createTextNode(unicode.zeroWidthSpace);
62
+ rangeEl.parentNode.insertBefore(con, rangeEl.parentNode.firstChild);
63
+ }
64
+ const offset = con.nodeType === 3 ? con.textContent.length : 1;
65
+ const children = formatEl.childNodes;
66
+ let after = con;
67
+ let child = children[0];
68
+ while ((child = children[0])) {
69
+ prev.insertBefore(child, after.nextSibling);
70
+ after = child;
71
+ }
72
+
73
+ dom.utils.removeItem(formatEl);
74
+ if (rangeEl.children.length === 0) dom.utils.removeItem(rangeEl);
75
+
76
+ ports.selection.setRange(con, offset, con, offset);
77
+ },
78
+ /** @action backspaceListRemoveNested */
79
+ 'backspace.list.removeNested': ({ ports }, { range }) => {
80
+ ports.html.remove();
81
+ if (range.startContainer.nodeType === 3) {
82
+ ports.selection.setRange(range.startContainer, range.startContainer.textContent.length, range.startContainer, range.startContainer.textContent.length);
83
+ }
84
+ },
85
+
86
+ // delete
87
+ /** @action deleteComponentSelect */
88
+ 'delete.component.select': ({ ports }, { formatEl, fileComponentInfo }) => {
89
+ if (dom.check.isListCell(formatEl)) {
90
+ const prev = fileComponentInfo.container.previousSibling;
91
+ if (dom.check.isZeroWidth(prev)) dom.utils.removeItem(prev);
92
+ } else if (dom.check.isZeroWidth(formatEl)) {
93
+ dom.utils.removeItem(formatEl);
94
+ }
95
+
96
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.editor.blur();
97
+ },
98
+ /** @action deleteComponentSelectNext */
99
+ 'delete.component.selectNext': ({ ports, ctx }, { formatEl, nextEl }) => {
100
+ if (dom.check.isZeroWidth(formatEl)) {
101
+ dom.utils.removeItem(formatEl);
102
+ // table component
103
+ if (dom.check.isTable(nextEl)) {
104
+ let cell = /** @type {HTMLElement} */ (dom.query.getEdgeChild(nextEl, dom.check.isTableCell, false));
105
+ cell = /** @type {HTMLElement} */ (cell.firstElementChild || cell);
106
+
107
+ ports.selection.setRange(cell, 0, cell, 0);
108
+ return;
109
+ }
110
+ }
111
+
112
+ // select file component
113
+ const fileComponentInfo = ports.component.get(nextEl);
114
+ if (fileComponentInfo) {
115
+ ctx.e.stopPropagation();
116
+ if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) ports.editor.blur();
117
+ } else if (ports.component.is(nextEl)) {
118
+ ctx.e.stopPropagation();
119
+ dom.utils.removeItem(nextEl);
120
+ }
121
+ },
122
+ /** @action deleteListRemoveNested */
123
+ 'delete.list.removeNested': ({ ports, ctx }, { range, formatEl, rangeEl }) => {
124
+ if (range.startContainer !== range.endContainer) ports.html.remove();
125
+
126
+ const next = /** @type {HTMLElement} */ (dom.utils.arrayFind(formatEl.children, dom.check.isList) || formatEl.nextElementSibling || rangeEl.parentElement.nextElementSibling);
127
+ if (next && (dom.check.isList(next) || dom.utils.arrayFind(next.children, dom.check.isList))) {
128
+ ctx.e.preventDefault();
129
+
130
+ let con, children;
131
+ if (dom.check.isList(next)) {
132
+ const child = next.firstElementChild;
133
+ children = child.childNodes;
134
+ con = children[0];
135
+ while (children[0]) {
136
+ formatEl.insertBefore(children[0], next);
137
+ }
138
+ dom.utils.removeItem(child);
139
+ } else {
140
+ con = next.firstChild;
141
+ children = next.childNodes;
142
+ while (children[0]) {
143
+ formatEl.appendChild(children[0]);
144
+ }
145
+ dom.utils.removeItem(next);
146
+ }
147
+
148
+ ports.selection.setRange(con, 0, con, 0);
149
+ ports.history.push(true);
150
+ }
151
+ },
152
+
153
+ // tab
154
+ /** @action tabFormatIndent */
155
+ 'tab.format.indent': ({ ports, ctx }, { range, formatEl, shift }) => {
156
+ const selectedFormats = ports.format.getLines(null);
157
+
158
+ const cells = [];
159
+ const lines = [];
160
+ const firstCell = dom.check.isListCell(selectedFormats[0]),
161
+ lastCell = dom.check.isListCell(selectedFormats.at(-1));
162
+ let r = {
163
+ sc: range.startContainer,
164
+ so: range.startOffset,
165
+ ec: range.endContainer,
166
+ eo: range.endOffset
167
+ };
168
+ for (let i = 0, len = selectedFormats.length, f; i < len; i++) {
169
+ f = selectedFormats[i];
170
+ if (dom.check.isListCell(f)) {
171
+ if (!f.previousElementSibling && !shift) {
172
+ continue;
173
+ } else {
174
+ cells.push(f);
175
+ }
176
+ } else {
177
+ lines.push(f);
178
+ }
179
+ }
180
+
181
+ // Nested list
182
+ if (cells.length > 0) {
183
+ r = ports.listFormat.applyNested(cells, shift);
184
+ }
185
+
186
+ // Lines tab
187
+ if (lines.length > 0) {
188
+ if (!shift) {
189
+ if (lines.length === 1) {
190
+ let tabSize = ctx.status.tabSize + 1;
191
+ if (ctx.options.get('syncTabIndent')) {
192
+ const baseIndex = dom.query.findTextIndexOnLine(formatEl, range.startContainer, range.startOffset, (current) => ports.component.is(current));
193
+ const prevTabEndIndex = ports.format.isLine(formatEl.previousElementSibling) ? dom.query.findTabEndIndex(formatEl.previousElementSibling, baseIndex, 2) : 0;
194
+ if (prevTabEndIndex > baseIndex) {
195
+ tabSize = prevTabEndIndex - baseIndex;
196
+ }
197
+ }
198
+
199
+ const tabText = dom.utils.createTextNode(new Array(tabSize).join('\u00A0'));
200
+ if (!ports.html.insertNode(tabText, { afterNode: null, skipCharCount: false })) return false;
201
+ if (!firstCell) {
202
+ r.sc = tabText;
203
+ r.so = tabText.length;
204
+ }
205
+ if (!lastCell) {
206
+ r.ec = tabText;
207
+ r.eo = tabText.length;
208
+ }
209
+ } else {
210
+ const tabText = dom.utils.createTextNode(new Array(ctx.status.tabSize + 1).join('\u00A0'));
211
+ const len = lines.length - 1;
212
+ for (let i = 0, child; i <= len; i++) {
213
+ child = lines[i].firstChild;
214
+ if (!child) continue;
215
+
216
+ if (dom.check.isBreak(child)) {
217
+ lines[i].insertBefore(tabText.cloneNode(false), child);
218
+ } else {
219
+ child.textContent = tabText.textContent + child.textContent;
220
+ }
221
+ }
222
+
223
+ const firstChild = dom.query.getEdgeChild(lines[0], 'text', false);
224
+ const endChild = dom.query.getEdgeChild(lines[len], 'text', true);
225
+ if (!firstCell && firstChild) {
226
+ r.sc = firstChild;
227
+ r.so = 0;
228
+ }
229
+ if (!lastCell && endChild) {
230
+ r.ec = endChild;
231
+ r.eo = endChild.textContent.length;
232
+ }
233
+ }
234
+ } else {
235
+ const len = lines.length - 1;
236
+ for (let i = 0, line; i <= len; i++) {
237
+ line = lines[i].childNodes;
238
+ for (let c = 0, cLen = line.length, child; c < cLen; c++) {
239
+ child = line[c];
240
+ if (!child) break;
241
+ if (dom.check.isZeroWidth(child)) continue;
242
+
243
+ if (/^\s{1,4}$/.test(child.textContent)) {
244
+ dom.utils.removeItem(child);
245
+ } else if (/^\s{1,4}/.test(child.textContent)) {
246
+ child.textContent = child.textContent.replace(/^\s{1,4}/, '');
247
+ }
248
+
249
+ break;
250
+ }
251
+ }
252
+
253
+ const firstChild = dom.query.getEdgeChild(lines[0], 'text', false);
254
+ const endChild = dom.query.getEdgeChild(lines[len], 'text', true);
255
+ if (!firstCell && firstChild) {
256
+ r.sc = firstChild;
257
+ r.so = 0;
258
+ }
259
+ if (!lastCell && endChild) {
260
+ r.ec = endChild;
261
+ r.eo = endChild.textContent.length;
262
+ }
263
+ }
264
+ }
265
+
266
+ ports.selection.setRange(r.sc, r.so, r.ec, r.eo);
267
+ },
268
+
269
+ // enter
270
+ /** @action enterScrollTo */
271
+ 'enter.scrollTo': ({ ports }, { range }) => {
272
+ ports.enterScrollTo(range);
273
+ },
274
+ /** @action enterLineAddDefault */
275
+ 'enter.line.addDefault': ({ ports, ctx }, { formatEl }) => {
276
+ const newFormat = ports.format.addLine(formatEl, ctx.options.get('defaultLine'));
277
+ const temp = newFormat.firstChild;
278
+ if (dom.check.isBreak(temp)) {
279
+ const zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
280
+ temp.parentNode.insertBefore(zeroWidth, temp);
281
+ ports.selection.setRange(zeroWidth, 1, zeroWidth, 1);
282
+ } else {
283
+ ports.selection.setRange(temp, 0, temp, 0);
284
+ }
285
+ },
286
+ /** @action enterListAddItem */
287
+ 'enter.list.addItem': ({ ports }, { formatEl, selectionNode }) => {
288
+ const br = dom.utils.createElement('BR');
289
+ const newEl = dom.utils.createElement('LI', null, br);
290
+
291
+ formatEl.parentNode.insertBefore(newEl, formatEl.nextElementSibling);
292
+ newEl.appendChild(selectionNode.nextSibling);
293
+
294
+ ports.selection.setRange(br, 1, br, 1);
295
+ },
296
+ /** @action enterFormatExitEmpty */
297
+ 'enter.format.exitEmpty': ({ ports, ctx }, { formatEl, rangeEl }) => {
298
+ let newEl = null;
299
+
300
+ if (dom.check.isListCell(rangeEl.parentElement)) {
301
+ const parentLi = formatEl.parentNode.parentElement;
302
+ rangeEl = parentLi.parentElement;
303
+ const newListCell = dom.utils.createElement('LI');
304
+ newListCell.innerHTML = '<br>';
305
+ dom.utils.copyTagAttributes(newListCell, formatEl, ctx.options.get('lineAttrReset'));
306
+ newEl = newListCell;
307
+ rangeEl.insertBefore(newEl, parentLi.nextElementSibling);
308
+ } else {
309
+ let newFormat;
310
+ if (dom.check.isTableCell(rangeEl.parentElement)) {
311
+ newFormat = 'DIV';
312
+ } else if (dom.check.isList(rangeEl.parentElement)) {
313
+ newFormat = 'LI';
314
+ } else if (ports.format.isLine(rangeEl.nextElementSibling)) {
315
+ newFormat = rangeEl.nextElementSibling.nodeName;
316
+ } else if (ports.format.isLine(rangeEl.previousElementSibling)) {
317
+ newFormat = rangeEl.previousElementSibling.nodeName;
318
+ } else {
319
+ newFormat = ctx.options.get('defaultLine');
320
+ }
321
+
322
+ newEl = dom.utils.createElement(newFormat);
323
+ const edge = ports.format.removeBlock(rangeEl, { selectedFormats: [formatEl], newBlockElement: null, shouldDelete: true, skipHistory: true });
324
+ edge.cc.insertBefore(newEl, edge.ec);
325
+ }
326
+
327
+ newEl.innerHTML = '<br>';
328
+ ports.nodeTransform.removeAllParents(formatEl, null, null);
329
+ ports.selection.setRange(newEl, 1, newEl, 1);
330
+ },
331
+ /** @action enterFormatCleanBrAndZWS */
332
+ 'enter.format.cleanBrAndZWS': ({ ports }, { selectionNode, selectionFormat, brBlock, children, offset }) => {
333
+ if (selectionFormat) dom.utils.removeItem(children[offset - 1]);
334
+ else dom.utils.removeItem(selectionNode);
335
+ const brBlockNext = /** @type {HTMLElement} */ (brBlock).nextElementSibling;
336
+ const newEl = ports.format.addLine(brBlock, ports.format.isLine(brBlockNext) ? brBlockNext : null);
337
+ dom.utils.copyFormatAttributes(newEl, brBlock);
338
+ ports.selection.setRange(newEl, 1, newEl, 1);
339
+ },
340
+ /** @action enterFormatInsertBrHtml */
341
+ 'enter.format.insertBrHtml': ({ ports }, { brBlock, range, wSelection, offset }) => {
342
+ ports.html.insert(range.collapsed && dom.check.isBreak(range.startContainer.childNodes[range.startOffset - 1]) ? '<br>' : '<br><br>', { selectInserted: false, skipCharCount: true, skipCleaning: true });
343
+
344
+ let focusNode = wSelection.focusNode;
345
+ const wOffset = wSelection.focusOffset;
346
+ if (brBlock === focusNode) {
347
+ focusNode = focusNode.childNodes[wOffset - offset > 1 ? wOffset - 1 : wOffset];
348
+ }
349
+
350
+ ports.selection.setRange(focusNode, 1, focusNode, 1);
351
+ ports.setOnShortcutKey(true);
352
+ },
353
+ /** @action enterFormatInsertBrNode */
354
+ 'enter.format.insertBrNode': ({ ports }, { wSelection }) => {
355
+ const focusNext = wSelection.focusNode.nextSibling;
356
+ const br = dom.utils.createElement('BR');
357
+ ports.html.insertNode(br, { afterNode: null, skipCharCount: true });
358
+
359
+ const brPrev = br.previousSibling,
360
+ brNext = br.nextSibling;
361
+ if (!dom.check.isBreak(focusNext) && !dom.check.isBreak(brPrev) && (!brNext || dom.check.isZeroWidth(brNext))) {
362
+ br.parentNode.insertBefore(br.cloneNode(false), br);
363
+ ports.selection.setRange(br, 1, br, 1);
364
+ } else {
365
+ ports.selection.setRange(brNext, 0, brNext, 0);
366
+ }
367
+
368
+ ports.setOnShortcutKey(true);
369
+ },
370
+ /** @action enterFormatBreakAtEdge */
371
+ 'enter.format.breakAtEdge': ({ ports, ctx }, { formatEl, selectionNode, formatStartEdge, formatEndEdge }) => {
372
+ const focusBR = dom.utils.createElement('BR');
373
+ const newFormat = dom.utils.createElement(formatEl.nodeName, null, focusBR);
374
+
375
+ dom.utils.copyTagAttributes(newFormat, formatEl, ctx.options.get('lineAttrReset'));
376
+
377
+ let child = focusBR;
378
+ let sNode = selectionNode;
379
+ do {
380
+ if (!dom.check.isBreak(sNode) && sNode.nodeType === 1) {
381
+ const f = /** @type {HTMLElement} */ (sNode.cloneNode(false));
382
+ f.appendChild(child);
383
+ child = f;
384
+ }
385
+ sNode = sNode.parentElement;
386
+ } while (formatEl !== sNode && formatEl.contains(sNode));
387
+
388
+ newFormat.appendChild(child);
389
+ formatEl.parentNode.insertBefore(newFormat, formatStartEdge && !formatEndEdge ? formatEl : formatEl.nextElementSibling);
390
+ if (formatEndEdge) {
391
+ ports.selection.setRange(focusBR, 1, focusBR, 1);
392
+ } else {
393
+ const firstEl = formatEl.firstChild || formatEl;
394
+ ports.selection.setRange(firstEl, 0, firstEl, 0);
395
+ }
396
+ },
397
+ /** @action enterFormatBreakWithSelection */
398
+ 'enter.format.breakWithSelection': ({ ports, ctx }, { formatEl, range, formatStartEdge, formatEndEdge }) => {
399
+ const isMultiLine = ports.format.getLine(range.startContainer, null) !== ports.format.getLine(range.endContainer, null);
400
+ const newFormat = /** @type {HTMLElement} */ (formatEl.cloneNode(false));
401
+ newFormat.innerHTML = '<br>';
402
+ const commonCon = /** @type {HTMLElement} */ (range.commonAncestorContainer);
403
+ const rcon =
404
+ commonCon === range.startContainer && commonCon === range.endContainer && dom.check.isZeroWidth(commonCon)
405
+ ? { container: commonCon, offset: range.endOffset, prevContainer: commonCon.previousElementSibling, commonCon: commonCon }
406
+ : ports.html.remove();
407
+
408
+ let newEl = ports.format.getLine(rcon.container, null);
409
+ let offset = 0;
410
+
411
+ if (!newEl) {
412
+ if (dom.check.isWysiwygFrame(rcon.container)) {
413
+ ports.enterPrevent(ctx.e);
414
+ ctx.fc.get('wysiwyg').appendChild(newFormat);
415
+ newEl = newFormat;
416
+ dom.utils.copyTagAttributes(newEl, formatEl, ctx.options.get('lineAttrReset'));
417
+ ports.selection.setRange(newEl, offset, newEl, offset);
418
+ }
419
+
420
+ return;
421
+ }
422
+
423
+ const innerRange = ports.format.getBlock(rcon.container);
424
+ newEl = newEl.contains(innerRange) ? dom.query.getEdgeChild(innerRange, (current) => Boolean(ports.format.getLine(current)), false) : newEl;
425
+ if (isMultiLine) {
426
+ if (formatEndEdge && !formatStartEdge) {
427
+ newEl.parentNode.insertBefore(newFormat, !rcon.prevContainer || rcon.container === rcon.prevContainer ? newEl.nextElementSibling : newEl);
428
+ newEl = newFormat;
429
+ offset = 0;
430
+ } else {
431
+ offset = rcon.offset;
432
+ if (formatStartEdge) {
433
+ const tempEl = newEl.parentNode.insertBefore(newFormat, newEl);
434
+ if (formatEndEdge) {
435
+ newEl = tempEl;
436
+ offset = 0;
437
+ }
438
+ }
439
+ }
440
+ } else {
441
+ if (formatEndEdge && formatStartEdge) {
442
+ newEl.parentNode.insertBefore(newFormat, rcon.prevContainer && rcon.container === rcon.prevContainer ? newEl.nextElementSibling : newEl);
443
+ newEl = newFormat;
444
+ offset = 0;
445
+ } else if (formatEndEdge) {
446
+ newEl = newEl.parentNode.insertBefore(newFormat, newEl.nextElementSibling);
447
+ newEl = newFormat;
448
+ offset = 0;
449
+ } else {
450
+ newEl = ports.nodeTransform.split(rcon.container, rcon.offset, dom.query.getNodeDepth(formatEl));
451
+ }
452
+ }
453
+
454
+ ports.enterPrevent(ctx.e);
455
+ dom.utils.copyTagAttributes(newEl, formatEl, ctx.options.get('lineAttrReset'));
456
+ ports.selection.setRange(newEl, offset, newEl, offset);
457
+ },
458
+ /** @action enterFormatBreakAtCursor */
459
+ 'enter.format.breakAtCursor': ({ ports, ctx }, { formatEl, range }) => {
460
+ let newEl = null;
461
+
462
+ if (dom.check.isZeroWidth(formatEl)) {
463
+ newEl = ports.format.addLine(formatEl, formatEl.cloneNode(false));
464
+ } else {
465
+ newEl = ports.nodeTransform.split(range.endContainer, range.endOffset, dom.query.getNodeDepth(formatEl));
466
+ }
467
+
468
+ ports.enterPrevent(ctx.e);
469
+ dom.utils.copyTagAttributes(newEl, formatEl, ctx.options.get('lineAttrReset'));
470
+ ports.selection.setRange(newEl, 0, newEl, 0);
471
+ },
472
+ /** @action enterFigcaptionExitInList */
473
+ 'enter.figcaption.exitInList': ({ ports }, { formatEl }) => {
474
+ const newEl = ports.format.addLine(formatEl, null);
475
+ ports.selection.setRange(newEl, 0, newEl, 0);
476
+ },
477
+
478
+ // keydown reducer
479
+ /** @action keydownInputInsertNbsp */
480
+ 'keydown.input.insertNbsp': ({ ports }) => {
481
+ const nbsp = ports.html.insertNode(dom.utils.createTextNode('\u00a0'), { afterNode: null, skipCharCount: true });
482
+ if (nbsp) {
483
+ ports.selection.setRange(nbsp, nbsp.length, nbsp, nbsp.length);
484
+ }
485
+ },
486
+ /** @action keydownInputInsertZWS */
487
+ 'keydown.input.insertZWS': ({ ports }) => {
488
+ const zeroWidth = dom.utils.createTextNode(unicode.zeroWidthSpace);
489
+ ports.html.insertNode(zeroWidth, { afterNode: null, skipCharCount: true });
490
+ ports.selection.setRange(zeroWidth, 1, zeroWidth, 1);
491
+ }
492
+ };
493
+
494
+ /**
495
+ * @param {HTMLElement} formatEl - Format element
496
+ * @returns {Node}
497
+ */
498
+ function LineDelete_next(formatEl) {
499
+ const focusNode = formatEl.lastChild;
500
+ const next = formatEl.nextElementSibling;
501
+
502
+ if (!next) return focusNode;
503
+
504
+ if (dom.check.isZeroWidth(next)) {
505
+ dom.utils.removeItem(next);
506
+ return focusNode;
507
+ }
508
+
509
+ const nextChild = next.childNodes;
510
+ while (nextChild[0]) {
511
+ formatEl.appendChild(nextChild[0]);
512
+ }
513
+
514
+ dom.utils.removeItem(next);
515
+
516
+ return focusNode;
517
+ }
518
+
519
+ /**
520
+ * @param {HTMLElement} formatEl - Format element
521
+ * @returns {{focusNode: Node, focusOffset: number}}
522
+ */
523
+ function LineDelete_prev(formatEl) {
524
+ const formatChild = formatEl.childNodes;
525
+ const prev = formatEl.previousElementSibling;
526
+ let focusNode = formatChild[0];
527
+ let focusOffset = 0;
528
+
529
+ if (!prev) return { focusNode, focusOffset };
530
+
531
+ if (dom.check.isZeroWidth(prev)) {
532
+ dom.utils.removeItem(prev);
533
+ return { focusNode, focusOffset };
534
+ }
535
+
536
+ if (formatChild.length > 1 || formatChild[0]?.textContent.length > 0) {
537
+ while (formatChild[0]) {
538
+ prev.appendChild(formatChild[0]);
539
+ }
540
+ } else {
541
+ focusNode = prev.lastChild;
542
+ focusOffset = focusNode.textContent.length;
543
+ }
544
+
545
+ dom.utils.removeItem(formatEl);
546
+
547
+ return { focusNode, focusOffset };
548
+ }
549
+
550
+ // test export
551
+ export { LineDelete_next, LineDelete_prev };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @file effects/ruleHelpers.js
3
+ *
4
+ * ⚠️ PATTERN COMPROMISE - Rule Helpers with Side Effects
5
+ *
6
+ * These functions are directly called from rules (not via actions/effects).
7
+ * This breaks pure reducer pattern but is necessary because they need to:
8
+ * 1. Perform atomic check + execute operations
9
+ * 2. Return values for rule conditional logic
10
+ *
11
+ * Categories:
12
+ * - QUERY (safe): isUneditableNode - reads DOM only
13
+ * - COMMAND (side effect): hardDelete, cleanRemovedTags - modifies DOM + returns status
14
+ */
15
+
16
+ import { dom } from '../../../helper';
17
+
18
+ /**
19
+ * @description Deletes specific elements such as tables in "Firefox" and media elements (image, video, audio) in "Chrome".
20
+ * - Handles deletion logic based on selection range and node types.
21
+ * @param {__se__EventPorts} ports - Reducer ports
22
+ * @returns {boolean} Returns `true` if an element was deleted and focus was adjusted, otherwise `false`.
23
+ */
24
+ function hardDelete(ports) {
25
+ const range = ports.selection.getRange();
26
+ const sc = range.startContainer;
27
+ const ec = range.endContainer;
28
+
29
+ // table
30
+ const sCell = ports.format.getBlock(sc);
31
+ const eCell = ports.format.getBlock(ec);
32
+ const sIsCell = dom.check.isTableCell(sCell);
33
+ const eIsCell = dom.check.isTableCell(eCell);
34
+ if (((sIsCell && !sCell.previousElementSibling && !sCell.parentElement.previousElementSibling) || (eIsCell && !eCell.nextElementSibling && !eCell.parentElement.nextElementSibling)) && sCell !== eCell) {
35
+ const ancestor = dom.query.getParentElement(range.commonAncestorContainer, dom.check.isFigure)?.parentElement || range.commonAncestorContainer;
36
+ if (!sIsCell) {
37
+ dom.utils.removeItem(dom.query.getParentElement(eCell, (current) => ancestor === current.parentNode));
38
+ } else if (!eIsCell) {
39
+ dom.utils.removeItem(dom.query.getParentElement(sCell, (current) => ancestor === current.parentNode));
40
+ } else {
41
+ dom.utils.removeItem(dom.query.getParentElement(sCell, (current) => ancestor === current.parentNode));
42
+ ports.editor._nativeFocus();
43
+ return true;
44
+ }
45
+ }
46
+
47
+ // component
48
+ const sComp = sc.nodeType === 1 ? dom.query.getParentElement(sc, '.se-component') : null;
49
+ const eComp = ec.nodeType === 1 ? dom.query.getParentElement(ec, '.se-component') : null;
50
+ if (sComp) dom.utils.removeItem(sComp);
51
+ if (eComp) dom.utils.removeItem(eComp);
52
+
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * @description Cleans up removed tags and normalizes DOM structure.
58
+ * Removes orphaned nodes that are outside the format element's valid range.
59
+ * @param {__se__EventPorts} ports - Reducer ports
60
+ * @param {Node} startCon - Starting container node to clean
61
+ * @param {Element} formatEl - Parent format element containing the structure
62
+ * @returns {boolean|undefined} Returns true if nodes were removed, undefined otherwise
63
+ */
64
+ function cleanRemovedTags(ports, startCon, formatEl) {
65
+ let prev = startCon.parentNode.previousSibling;
66
+ const next = startCon.parentNode.nextSibling;
67
+ if (!prev) {
68
+ if (!next) {
69
+ prev = dom.utils.createElement('BR');
70
+ formatEl.appendChild(prev);
71
+ } else {
72
+ prev = next;
73
+ }
74
+ }
75
+
76
+ let con = startCon;
77
+ while (formatEl.contains(con) && !con.previousSibling) {
78
+ con = con.parentNode;
79
+ }
80
+
81
+ if (!formatEl.contains(con)) {
82
+ startCon.textContent = '';
83
+ ports.nodeTransform.removeAllParents(startCon, null, formatEl);
84
+ return true;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * @description Determines if the "range" is within an uneditable node.
90
+ * @param {__se__EventPorts} ports - Reducer ports
91
+ * @param {Range} range The range object
92
+ * @param {boolean} isFront Whether to check the start or end of the range
93
+ * @returns {Node|null} The uneditable node if found, otherwise null
94
+ */
95
+ function isUneditableNode(ports, range, isFront) {
96
+ const container = isFront ? range.startContainer : range.endContainer;
97
+ const offset = isFront ? range.startOffset : range.endOffset;
98
+ const siblingKey = isFront ? 'previousSibling' : 'nextSibling';
99
+ const isElement = container.nodeType === 1;
100
+
101
+ let siblingNode;
102
+ if (isElement) {
103
+ siblingNode = /** @type {HTMLElement} */ (_isUneditableNode_getSibling(ports, container.childNodes[offset], siblingKey, container));
104
+ return dom.check.isComponentContainer(siblingNode) || dom.check.isNonEditable(siblingNode) ? siblingNode : null;
105
+ } else {
106
+ siblingNode = /** @type {HTMLElement} */ (_isUneditableNode_getSibling(ports, container, siblingKey, container));
107
+ return dom.check.isEdgePoint(container, offset, isFront ? 'front' : 'end') && (dom.check.isComponentContainer(siblingNode) || dom.check.isNonEditable(siblingNode)) ? siblingNode : null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * @private
113
+ * @description Retrieves the sibling node of a selected node if it is uneditable. || component node.
114
+ * - Used only in `_isUneditableNode`.
115
+ * @param {__se__EventPorts} ports - Reducer ports
116
+ * @param {Node} selectNode The selected node
117
+ * @param {string} siblingKey The key to access the sibling (`previousSibling` or `nextSibling`)
118
+ * @param {Node} container The parent container node
119
+ * @returns {Node|null} The sibling node if found, otherwise null
120
+ */
121
+ function _isUneditableNode_getSibling(ports, selectNode, siblingKey, container) {
122
+ if (!selectNode) return null;
123
+ let siblingNode = selectNode[siblingKey];
124
+
125
+ if (!siblingNode) {
126
+ siblingNode = ports.format.getLine(container);
127
+ siblingNode = siblingNode ? siblingNode[siblingKey] : null;
128
+ if (siblingNode && !ports.component.is(siblingNode)) siblingNode = siblingKey === 'previousSibling' ? siblingNode.firstChild : siblingNode.lastChild;
129
+ else return null;
130
+ }
131
+
132
+ return siblingNode;
133
+ }
134
+
135
+ /**
136
+ * @description Excute eventManager._setDefaultLine
137
+ * @param {__se__EventPorts} ports - Reducer ports
138
+ * @param {string} lineTagName - line tag name
139
+ * @returns {void|null}
140
+ */
141
+ function setDefaultLine(ports, lineTagName) {
142
+ return ports.setDefaultLine(lineTagName);
143
+ }
144
+
145
+ export { hardDelete, cleanRemovedTags, isUneditableNode, setDefaultLine };