suneditor 3.3.1 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/suneditor.min.css +1 -1
- package/dist/suneditor.min.js +1 -1
- package/package.json +3 -2
- package/src/assets/suneditor.css +19 -3
- package/src/core/editor.js +6 -1
- package/src/core/event/effects/keydown.registry.js +14 -9
- package/src/core/event/effects/ruleHelpers.js +119 -1
- package/src/core/event/eventOrchestrator.js +15 -6
- package/src/core/event/rules/keydown.rule.backspace.js +10 -47
- package/src/core/event/rules/keydown.rule.delete.js +28 -66
- package/src/core/logic/dom/format.js +1 -1
- package/src/core/logic/dom/html.js +54 -5
- package/src/core/logic/dom/nodeTransform.js +5 -3
- package/src/core/logic/shell/ui.js +6 -1
- package/src/core/schema/options.js +4 -3
- package/src/events.js +1 -1
- package/src/helper/dom/domQuery.js +10 -4
- package/src/helper/googleDocs.js +40 -0
- package/src/helper/index.js +3 -0
- package/types/core/event/effects/ruleHelpers.d.ts +56 -0
- package/types/core/schema/options.d.ts +6 -5
- package/types/events.d.ts +2 -2
- package/types/helper/dom/domQuery.d.ts +4 -2
- package/types/helper/googleDocs.d.ts +19 -0
- package/types/helper/index.d.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suneditor",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"description": "Vanilla JavaScript based WYSIWYG web editor",
|
|
5
5
|
"author": "Yi JiHong",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"node": ">=14.0.0"
|
|
51
51
|
},
|
|
52
52
|
"overrides": {
|
|
53
|
-
"uuid": ">=14.0.0"
|
|
53
|
+
"uuid": ">=14.0.0",
|
|
54
|
+
"qs": "^6.16.0"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
57
|
"dev": "webpack-dev-server --config webpack/dev.js",
|
package/src/assets/suneditor.css
CHANGED
|
@@ -2112,11 +2112,18 @@
|
|
|
2112
2112
|
-moz-backface-visibility: hidden;
|
|
2113
2113
|
}
|
|
2114
2114
|
|
|
2115
|
+
.sun-editor .se-wrapper .se-placeholder-line {
|
|
2116
|
+
position: relative;
|
|
2117
|
+
}
|
|
2115
2118
|
.sun-editor .se-wrapper .se-placeholder-line::before {
|
|
2116
2119
|
content: attr(data-se-placeholder-line);
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
+
position: absolute;
|
|
2121
|
+
inset: 0;
|
|
2122
|
+
padding: inherit;
|
|
2123
|
+
box-sizing: border-box;
|
|
2124
|
+
text-align: inherit;
|
|
2125
|
+
overflow: hidden;
|
|
2126
|
+
text-overflow: ellipsis;
|
|
2120
2127
|
color: var(--se-main-outline-color);
|
|
2121
2128
|
pointer-events: none;
|
|
2122
2129
|
user-select: none;
|
|
@@ -2124,6 +2131,15 @@
|
|
|
2124
2131
|
white-space: nowrap;
|
|
2125
2132
|
}
|
|
2126
2133
|
|
|
2134
|
+
.sun-editor .se-wrapper li.se-placeholder-line {
|
|
2135
|
+
overflow: hidden;
|
|
2136
|
+
}
|
|
2137
|
+
.sun-editor .se-wrapper li.se-placeholder-line::before {
|
|
2138
|
+
inset: auto;
|
|
2139
|
+
padding: 0;
|
|
2140
|
+
max-width: 100%;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2127
2143
|
/** --- status bar */
|
|
2128
2144
|
.sun-editor .se-status-bar {
|
|
2129
2145
|
display: flex;
|
package/src/core/editor.js
CHANGED
|
@@ -232,7 +232,12 @@ class Editor {
|
|
|
232
232
|
}
|
|
233
233
|
if (e.get('documentType').usePage) {
|
|
234
234
|
e.set('documentType_use_page', true);
|
|
235
|
-
|
|
235
|
+
const mirror = e.get('documentTypePageMirror');
|
|
236
|
+
const frag = mirror.ownerDocument.createDocumentFragment();
|
|
237
|
+
for (let n = e.get('wysiwyg').firstChild; n; n = n.nextSibling) {
|
|
238
|
+
frag.appendChild(n.cloneNode(true));
|
|
239
|
+
}
|
|
240
|
+
mirror.replaceChildren(frag);
|
|
236
241
|
}
|
|
237
242
|
}
|
|
238
243
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dom, unicode } from '../../../helper';
|
|
2
|
+
import { getNestedListTarget } from './ruleHelpers';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* @typedef {Object} EffectContext_keydown
|
|
@@ -79,12 +80,16 @@ export default {
|
|
|
79
80
|
/** @action backspaceComponentSelect */
|
|
80
81
|
'backspace.component.select': ({ ports }, { selectionNode, range, fileComponentInfo }) => {
|
|
81
82
|
let currentZWS = null;
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
|
|
84
|
+
if (dom.check.isBreak(selectionNode)) {
|
|
85
|
+
dom.utils.removeItem(selectionNode);
|
|
86
|
+
} else if (dom.check.isBreak((currentZWS = range.startContainer.childNodes?.[range.startOffset]))) {
|
|
84
87
|
dom.utils.removeItem(currentZWS);
|
|
88
|
+
}
|
|
85
89
|
|
|
86
|
-
if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false)
|
|
90
|
+
if (ports.component.select(fileComponentInfo.target, fileComponentInfo.pluginName) === false) {
|
|
87
91
|
ports.focusManager.blur();
|
|
92
|
+
}
|
|
88
93
|
},
|
|
89
94
|
|
|
90
95
|
/** @action backspaceComponentRemove */
|
|
@@ -103,6 +108,9 @@ export default {
|
|
|
103
108
|
rangeEl.parentNode.insertBefore(con, rangeEl.parentNode.firstChild);
|
|
104
109
|
}
|
|
105
110
|
const offset = con.nodeType === 3 ? con.textContent.length : 1;
|
|
111
|
+
|
|
112
|
+
stripTrailingBreaks(formatEl);
|
|
113
|
+
|
|
106
114
|
const children = formatEl.childNodes;
|
|
107
115
|
let after = con;
|
|
108
116
|
let child = children[0];
|
|
@@ -213,12 +221,9 @@ export default {
|
|
|
213
221
|
'delete.list.removeNested': ({ ports, ctx }, { range, formatEl, rangeEl }) => {
|
|
214
222
|
if (range.startContainer !== range.endContainer) ports.html.remove();
|
|
215
223
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
rangeEl.parentElement.nextElementSibling
|
|
220
|
-
);
|
|
221
|
-
if (next && (dom.check.isList(next) || dom.utils.arrayFind(next.children, dom.check.isList))) {
|
|
224
|
+
// Same decision the rule gated on — asked once, here and there, so the two can't drift apart.
|
|
225
|
+
const next = getNestedListTarget(formatEl, rangeEl);
|
|
226
|
+
if (next) {
|
|
222
227
|
ctx.e.preventDefault();
|
|
223
228
|
|
|
224
229
|
let con, children;
|
|
@@ -188,4 +188,122 @@ function isRtlBidiMismatch(range, formatEl, detectedEdge, doc) {
|
|
|
188
188
|
return detectedEdge === 'front' ? caretRect.left <= contentRect.left + 2 : caretRect.left >= contentRect.right - 2;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
/**
|
|
192
|
+
* @description Whether the caret sits on a bare `<br>` that stands at the front/end edge of its `line`.
|
|
193
|
+
* @param {Range} range - The current range
|
|
194
|
+
* @param {Node} selectionNode - Current selection node
|
|
195
|
+
* @param {'front'|'end'} edge - Edge to test: `front` for Backspace, `end` for Delete
|
|
196
|
+
* @returns {boolean} `true` if the caret is on an edge `<br>`
|
|
197
|
+
*/
|
|
198
|
+
function isEdgeBreakCaret(range, selectionNode, edge) {
|
|
199
|
+
if (!range.collapsed || !dom.check.isBreak(selectionNode)) return false;
|
|
200
|
+
|
|
201
|
+
const key = edge === 'front' ? 'previousSibling' : 'nextSibling';
|
|
202
|
+
let sibling = selectionNode[key];
|
|
203
|
+
while (sibling?.nodeType === 3 && dom.check.isZeroWidth(sibling)) {
|
|
204
|
+
sibling = sibling[key];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return !sibling || dom.check.isList(sibling);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @description The previous/next element in document order — crossing in and out of blocks (list, quote).
|
|
212
|
+
* - Out of list-cell ancestors (a nested list), stopping at a closure block (table cell).
|
|
213
|
+
* @param {EventPorts['format']} format - Format module
|
|
214
|
+
* @param {?HTMLElement} line - The caret's `line` element
|
|
215
|
+
* @param {'front'|'end'} edge - `front` for the previous element, `end` for the next one
|
|
216
|
+
* @returns {?HTMLElement} The adjacent element, or `null` at the document edge
|
|
217
|
+
*/
|
|
218
|
+
function getAdjacentElement(format, line, edge) {
|
|
219
|
+
const siblingKey = edge === 'front' ? 'previousElementSibling' : 'nextElementSibling';
|
|
220
|
+
const childKey = edge === 'front' ? 'lastElementChild' : 'firstElementChild';
|
|
221
|
+
|
|
222
|
+
let node = line;
|
|
223
|
+
let adjacent = null;
|
|
224
|
+
while (node && !(adjacent = node[siblingKey])) {
|
|
225
|
+
const parent = node.parentElement;
|
|
226
|
+
if (!parent || format.isClosureBlock(parent) || dom.check.isWysiwygFrame(parent)) return null;
|
|
227
|
+
|
|
228
|
+
if (format.isBlock(parent)) {
|
|
229
|
+
node = parent;
|
|
230
|
+
} else if (dom.check.isListCell(parent)) {
|
|
231
|
+
if (edge === 'front') return /** @type {HTMLElement} */ (parent);
|
|
232
|
+
node = parent;
|
|
233
|
+
} else {
|
|
234
|
+
return null; // the editable root
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// step into blocks down to the nearest line
|
|
239
|
+
while (adjacent && format.isBlock(adjacent) && !format.isClosureBlock(adjacent)) {
|
|
240
|
+
adjacent = adjacent[childKey];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return /** @type {?HTMLElement} */ (adjacent);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @description The previous/next `line` in document order — {@link getAdjacentElement} filtered to lines.
|
|
248
|
+
* - `null` means either the document edge or a non-`line` neighbour (a component); use
|
|
249
|
+
* {@link getAdjacentElement} when the two must be told apart.
|
|
250
|
+
* @param {EventPorts['format']} format - Format module
|
|
251
|
+
* @param {?HTMLElement} line - The caret's `line` element
|
|
252
|
+
* @param {'front'|'end'} edge - `front` for the previous line, `end` for the next one
|
|
253
|
+
* @returns {?HTMLElement} The adjacent line, or `null`
|
|
254
|
+
*/
|
|
255
|
+
function getAdjacentLine(format, line, edge) {
|
|
256
|
+
const adjacent = getAdjacentElement(format, line, edge);
|
|
257
|
+
return format.isLine(adjacent) ? /** @type {HTMLElement} */ (adjacent) : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @description The neighbouring `line` an empty line collapses into, or `null` when there is nothing to merge.
|
|
262
|
+
* - A cell owning a nested list belongs to {@link getNestedListTarget} instead.
|
|
263
|
+
* @param {EventPorts['format']} format - Format module
|
|
264
|
+
* @param {?HTMLElement} formatEl - The caret's `line` element
|
|
265
|
+
* @param {'front'|'end'} edge - `front` for Backspace (previous line), `end` for Delete (next line)
|
|
266
|
+
* @returns {?HTMLElement} The neighbouring line to merge into, or `null`
|
|
267
|
+
*/
|
|
268
|
+
function getEmptyLineMergeTarget(format, formatEl, edge) {
|
|
269
|
+
if (!formatEl || !format.isNormalLine(formatEl) || !dom.check.isEmptyLine(formatEl)) return null;
|
|
270
|
+
if (dom.utils.arrayFind(formatEl.children, dom.check.isList)) return null;
|
|
271
|
+
|
|
272
|
+
const neighbor = /** @type {HTMLElement} */ (
|
|
273
|
+
edge === 'front' ? formatEl.previousElementSibling : formatEl.nextElementSibling
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
return format.isNormalLine(neighbor) || format.isBrLine(neighbor) ? neighbor : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @description The nested list a list-cell Backspace/Delete would lift, or `null` when there is none.
|
|
281
|
+
* - The rules gate their list branch on it so the branch can't claim the key with nothing to do.
|
|
282
|
+
* @param {HTMLElement} formatEl - The caret's list cell
|
|
283
|
+
* @param {HTMLElement} rangeEl - The list (`UL`/`OL`) the cell belongs to
|
|
284
|
+
* @returns {?HTMLElement} The element carrying the nested list, or `null`
|
|
285
|
+
*/
|
|
286
|
+
function getNestedListTarget(formatEl, rangeEl) {
|
|
287
|
+
const next = /** @type {HTMLElement} */ (
|
|
288
|
+
dom.utils.arrayFind(formatEl.children, dom.check.isList) ||
|
|
289
|
+
formatEl.nextElementSibling ||
|
|
290
|
+
rangeEl?.parentElement?.nextElementSibling
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (!next) return null;
|
|
294
|
+
|
|
295
|
+
return dom.check.isList(next) || dom.utils.arrayFind(next.children, dom.check.isList) ? next : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export {
|
|
299
|
+
hardDelete,
|
|
300
|
+
cleanRemovedTags,
|
|
301
|
+
isUneditableNode,
|
|
302
|
+
setDefaultLine,
|
|
303
|
+
isRtlBidiMismatch,
|
|
304
|
+
isEdgeBreakCaret,
|
|
305
|
+
getAdjacentElement,
|
|
306
|
+
getAdjacentLine,
|
|
307
|
+
getEmptyLineMergeTarget,
|
|
308
|
+
getNestedListTarget,
|
|
309
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import KernelInjector from '../kernel/kernelInjector';
|
|
2
|
-
import { dom, unicode, numbers, env, converter, msOffice } from '../../helper';
|
|
2
|
+
import { dom, unicode, numbers, env, converter, msOffice, googleDocs } from '../../helper';
|
|
3
3
|
import { _DragHandle } from '../../modules/ui';
|
|
4
4
|
|
|
5
5
|
// event handlers
|
|
@@ -234,9 +234,14 @@ class EventOrchestrator extends KernelInjector {
|
|
|
234
234
|
r.startContainer.nodeType === 3 &&
|
|
235
235
|
dom.check.isZeroWidth(r.startContainer)
|
|
236
236
|
) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
const zeroWidth = r.startContainer;
|
|
238
|
+
const br = zeroWidth.nextSibling;
|
|
239
|
+
if (br && dom.check.isBreak(br)) {
|
|
240
|
+
dom.utils.removeItem(zeroWidth);
|
|
241
|
+
this.$.selection.setRange(br, 0, br, 0);
|
|
242
|
+
} else {
|
|
243
|
+
this.$.selection.setRange(r.endContainer, r.endOffset, r.endContainer, r.endOffset);
|
|
244
|
+
}
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
return this.$.selection.getNode();
|
|
@@ -269,7 +274,7 @@ class EventOrchestrator extends KernelInjector {
|
|
|
269
274
|
/**
|
|
270
275
|
* @internal
|
|
271
276
|
* @description Processes clipboard data for `paste` and `drop` events, handling text and HTML cleanup.
|
|
272
|
-
* - Supports specific handling for content from Microsoft Office applications.
|
|
277
|
+
* - Supports specific handling for content from Microsoft Office applications and Google Docs.
|
|
273
278
|
* @param {"paste"|"drop"} type The type of event
|
|
274
279
|
* @param {Event} e The original event object
|
|
275
280
|
* @param {DataTransfer} clipboardData The clipboard data object
|
|
@@ -292,8 +297,10 @@ class EventOrchestrator extends KernelInjector {
|
|
|
292
297
|
/content=["']*Word.Document/i.test(cleanData) ||
|
|
293
298
|
/content=["']*OneNote.File/i.test(cleanData) ||
|
|
294
299
|
/content=["']*Excel.Sheet/i.test(cleanData);
|
|
300
|
+
// Google Docs
|
|
301
|
+
const GDData = !SEData && !MSData && googleDocs.isGoogleDocs(cleanData);
|
|
295
302
|
// from
|
|
296
|
-
const from = SEData ? 'SE' : MSData ? 'MS' : '';
|
|
303
|
+
const from = SEData ? 'SE' : MSData ? 'MS' : GDData ? 'GOOGLE' : '';
|
|
297
304
|
|
|
298
305
|
if (onlyText) {
|
|
299
306
|
cleanData = converter.htmlToEntity(plainText).replace(/\n/g, '<br>');
|
|
@@ -306,6 +313,8 @@ class EventOrchestrator extends KernelInjector {
|
|
|
306
313
|
cleanData = cleanData.replace(/\n/g, ' ');
|
|
307
314
|
plainText = plainText.replace(/\n/g, ' ');
|
|
308
315
|
cleanData = msOffice.cleanHTML(cleanData);
|
|
316
|
+
} else if (GDData) {
|
|
317
|
+
cleanData = googleDocs.cleanHTML(cleanData);
|
|
309
318
|
}
|
|
310
319
|
}
|
|
311
320
|
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
isUneditableNode,
|
|
6
6
|
setDefaultLine,
|
|
7
7
|
isRtlBidiMismatch,
|
|
8
|
+
isEdgeBreakCaret,
|
|
9
|
+
getEmptyLineMergeTarget,
|
|
10
|
+
getAdjacentLine,
|
|
8
11
|
} from '../effects/ruleHelpers';
|
|
9
12
|
import { A } from '../actions';
|
|
10
13
|
|
|
@@ -79,8 +82,8 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
79
82
|
!dom.check.isListCell(formatEl) &&
|
|
80
83
|
format.isEdgeLine(range.startContainer, range.startOffset, 'front')
|
|
81
84
|
) {
|
|
82
|
-
const prevLine =
|
|
83
|
-
if (prevLine) {
|
|
85
|
+
const prevLine = getAdjacentLine(format, formatEl, 'front');
|
|
86
|
+
if (prevLine && prevLine.parentElement !== formatEl.parentElement && format.isNormalLine(prevLine)) {
|
|
84
87
|
actions.push(A.preventStop());
|
|
85
88
|
actions.push(A.mergeLineInto(prevLine, formatEl));
|
|
86
89
|
actions.push(A.historyPush(true));
|
|
@@ -152,6 +155,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
152
155
|
: dom.check.isEdgePoint(range.startContainer, range.startOffset)
|
|
153
156
|
? dom.query.getPreviousDeepestNode(range.startContainer)
|
|
154
157
|
: null;
|
|
158
|
+
|
|
155
159
|
if (component.is(sel)) {
|
|
156
160
|
const fileComponentInfo = component.get(sel);
|
|
157
161
|
if (fileComponentInfo) {
|
|
@@ -185,6 +189,7 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
185
189
|
dom.check.isList(rangeEl) &&
|
|
186
190
|
(dom.check.isListCell(rangeEl.parentElement) || formatEl.previousElementSibling) &&
|
|
187
191
|
(selectionNode === formatEl ||
|
|
192
|
+
isEdgeBreakCaret(range, selectionNode, 'front') ||
|
|
188
193
|
(selectionNode.nodeType === 3 &&
|
|
189
194
|
(!selectionNode.previousSibling || dom.check.isList(selectionNode.previousSibling)))) &&
|
|
190
195
|
(format.getLine(range.startContainer, null) !== format.getLine(range.endContainer, null)
|
|
@@ -344,16 +349,9 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
344
349
|
return false;
|
|
345
350
|
}
|
|
346
351
|
|
|
347
|
-
// empty line: merge into the previous line
|
|
348
|
-
const emptyLinePrev = formatEl
|
|
349
|
-
if (
|
|
350
|
-
!selectRange &&
|
|
351
|
-
formatEl &&
|
|
352
|
-
format.isNormalLine(formatEl) &&
|
|
353
|
-
!dom.check.isListCell(formatEl) &&
|
|
354
|
-
dom.check.isEmptyLine(formatEl) &&
|
|
355
|
-
(format.isNormalLine(emptyLinePrev) || format.isBrLine(emptyLinePrev))
|
|
356
|
-
) {
|
|
352
|
+
// empty line: merge into the previous line — plain lines and list cells alike
|
|
353
|
+
const emptyLinePrev = getEmptyLineMergeTarget(format, formatEl, 'front');
|
|
354
|
+
if (!selectRange && emptyLinePrev) {
|
|
357
355
|
actions.push(A.preventStop());
|
|
358
356
|
actions.push(A.backspaceEmptyLineMergePrev(formatEl, emptyLinePrev));
|
|
359
357
|
actions.push(A.historyPush(true));
|
|
@@ -364,38 +362,3 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
364
362
|
actions.push(A.caretScrollTo(range));
|
|
365
363
|
return true;
|
|
366
364
|
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* @description Mirror of the Delete rule's `findNextMergeLine`: finds the previous line to merge INTO when
|
|
370
|
-
* Backspace is pressed at the start of `formatEl` and the previous line in document order lies across a block
|
|
371
|
-
* boundary. Returns `null` for a plain line→line neighbour (left to native) or when nothing is safely mergeable
|
|
372
|
-
* (list cell, closure, brLine, start of document).
|
|
373
|
-
* @param {EventPorts['format']} format
|
|
374
|
-
* @param {HTMLElement} formatEl
|
|
375
|
-
* @returns {?HTMLElement}
|
|
376
|
-
*/
|
|
377
|
-
function findPrevMergeLine(format, formatEl) {
|
|
378
|
-
let prev = formatEl.previousElementSibling;
|
|
379
|
-
if (!prev) {
|
|
380
|
-
// `formatEl` is the first line of its block — look at what precedes the block.
|
|
381
|
-
const block = formatEl.parentElement;
|
|
382
|
-
if (!format.isBlock(block) || format.isClosureBlock(block)) return null;
|
|
383
|
-
prev = block.previousElementSibling;
|
|
384
|
-
if (!prev) return null;
|
|
385
|
-
} else if (!format.isBlock(prev)) {
|
|
386
|
-
return null; // plain line→line — let the browser merge it natively
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Descend into blocks to the LAST line; only merge into a simple, non-list, non-closure, non-brLine line.
|
|
390
|
-
let line = prev;
|
|
391
|
-
while (line && format.isBlock(line) && !format.isClosureBlock(line)) line = line.lastElementChild;
|
|
392
|
-
if (
|
|
393
|
-
!format.isNormalLine(line) ||
|
|
394
|
-
dom.check.isListCell(line) ||
|
|
395
|
-
format.isBrLine(line) ||
|
|
396
|
-
format.isClosureBrLine(line)
|
|
397
|
-
) {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
return /** @type {HTMLElement} */ (line);
|
|
401
|
-
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { dom } from '../../../helper';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
hardDelete,
|
|
4
|
+
isUneditableNode,
|
|
5
|
+
isRtlBidiMismatch,
|
|
6
|
+
isEdgeBreakCaret,
|
|
7
|
+
getEmptyLineMergeTarget,
|
|
8
|
+
getNestedListTarget,
|
|
9
|
+
getAdjacentElement,
|
|
10
|
+
} from '../effects/ruleHelpers';
|
|
3
11
|
import { A } from '../actions';
|
|
4
12
|
|
|
5
13
|
/**
|
|
@@ -37,31 +45,24 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
37
45
|
return true;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
!
|
|
43
|
-
formatEl &&
|
|
44
|
-
format.isNormalLine(formatEl) &&
|
|
45
|
-
!dom.check.isListCell(formatEl) &&
|
|
46
|
-
format.isEdgeLine(range.endContainer, range.endOffset, 'end')
|
|
47
|
-
) {
|
|
48
|
-
const targetLine = findNextMergeLine(format, formatEl);
|
|
49
|
-
if (targetLine) {
|
|
48
|
+
if (!selectRange && !bidiNotEnd && formatEl && format.isEdgeLine(range.endContainer, range.endOffset, 'end')) {
|
|
49
|
+
const adjacent = getAdjacentElement(format, formatEl, 'end');
|
|
50
|
+
if (!adjacent) {
|
|
50
51
|
actions.push(A.preventStop());
|
|
51
|
-
actions.push(A.mergeLineInto(formatEl, targetLine));
|
|
52
|
-
actions.push(A.historyPush(true));
|
|
53
52
|
return false;
|
|
54
53
|
}
|
|
55
|
-
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
if (
|
|
56
|
+
format.isLine(adjacent) &&
|
|
57
|
+
adjacent.parentElement !== formatEl.parentElement &&
|
|
58
|
+
format.isNormalLine(formatEl) &&
|
|
59
|
+
format.isNormalLine(adjacent)
|
|
60
|
+
) {
|
|
61
|
+
actions.push(A.preventStop());
|
|
62
|
+
actions.push(A.mergeLineInto(formatEl, adjacent));
|
|
63
|
+
actions.push(A.historyPush(true));
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// line delete
|
|
@@ -158,7 +159,9 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
158
159
|
if (
|
|
159
160
|
dom.check.isListCell(formatEl) &&
|
|
160
161
|
dom.check.isList(rangeEl) &&
|
|
162
|
+
(range.startContainer !== range.endContainer || getNestedListTarget(formatEl, rangeEl)) &&
|
|
161
163
|
(selectionNode === formatEl ||
|
|
164
|
+
isEdgeBreakCaret(range, selectionNode, 'end') ||
|
|
162
165
|
(selectionNode.nodeType === 3 &&
|
|
163
166
|
(!selectionNode.nextSibling || dom.check.isList(selectionNode.nextSibling)) &&
|
|
164
167
|
(format.getLine(range.startContainer, null) !== format.getLine(range.endContainer, null)
|
|
@@ -215,16 +218,9 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
// empty line
|
|
219
|
-
const emptyLineNext = formatEl
|
|
220
|
-
if (
|
|
221
|
-
!selectRange &&
|
|
222
|
-
formatEl &&
|
|
223
|
-
format.isNormalLine(formatEl) &&
|
|
224
|
-
!dom.check.isListCell(formatEl) &&
|
|
225
|
-
dom.check.isEmptyLine(formatEl) &&
|
|
226
|
-
(format.isNormalLine(emptyLineNext) || format.isBrLine(emptyLineNext))
|
|
227
|
-
) {
|
|
221
|
+
// empty line — plain lines and list cells alike
|
|
222
|
+
const emptyLineNext = getEmptyLineMergeTarget(format, formatEl, 'end');
|
|
223
|
+
if (!selectRange && emptyLineNext) {
|
|
228
224
|
actions.push(A.preventStop());
|
|
229
225
|
actions.push(A.deleteEmptyLineMergeNext(formatEl, emptyLineNext));
|
|
230
226
|
actions.push(A.historyPush(true));
|
|
@@ -233,37 +229,3 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
233
229
|
|
|
234
230
|
return true;
|
|
235
231
|
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* @description Finds the line to merge up when Delete is pressed at the end of `formatEl` and the next line in
|
|
239
|
-
* document order lies across a block boundary. Returns `null` for a plain line→line neighbour (left to native)
|
|
240
|
-
* or when there is nothing safely mergeable (list cell, closure, brLine, end of document).
|
|
241
|
-
* @param {EventPorts['format']} format
|
|
242
|
-
* @param {HTMLElement} formatEl
|
|
243
|
-
* @returns {?HTMLElement}
|
|
244
|
-
*/
|
|
245
|
-
function findNextMergeLine(format, formatEl) {
|
|
246
|
-
let next = formatEl.nextElementSibling;
|
|
247
|
-
if (!next) {
|
|
248
|
-
// `formatEl` is the last line of its block — look at what follows the block.
|
|
249
|
-
const block = formatEl.parentElement;
|
|
250
|
-
if (!format.isBlock(block) || format.isClosureBlock(block)) return null;
|
|
251
|
-
next = block.nextElementSibling;
|
|
252
|
-
if (!next) return null;
|
|
253
|
-
} else if (!format.isBlock(next)) {
|
|
254
|
-
return null; // plain line→line — let the browser merge it natively
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Descend into blocks to the first line; only merge a simple, non-list, non-closure, non-brLine line.
|
|
258
|
-
let line = next;
|
|
259
|
-
while (line && format.isBlock(line) && !format.isClosureBlock(line)) line = line.firstElementChild;
|
|
260
|
-
if (
|
|
261
|
-
!format.isNormalLine(line) ||
|
|
262
|
-
dom.check.isListCell(line) ||
|
|
263
|
-
format.isBrLine(line) ||
|
|
264
|
-
format.isClosureBrLine(line)
|
|
265
|
-
) {
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
return /** @type {HTMLElement} */ (line);
|
|
269
|
-
}
|
|
@@ -74,7 +74,7 @@ class Format {
|
|
|
74
74
|
) {
|
|
75
75
|
newFormat = /** @type {HTMLElement} */ (element.cloneNode(false));
|
|
76
76
|
dom.utils.copyFormatAttributes(newFormat, node);
|
|
77
|
-
newFormat.
|
|
77
|
+
while (node.firstChild) newFormat.appendChild(node.firstChild);
|
|
78
78
|
|
|
79
79
|
node.parentNode.replaceChild(newFormat, node);
|
|
80
80
|
}
|
|
@@ -9,6 +9,7 @@ const ZWS_RUN_REGEXP = new RegExp(unicode.zeroWidthSpace + '+', 'g');
|
|
|
9
9
|
* @description All HTML related classes involved in the editing area
|
|
10
10
|
*/
|
|
11
11
|
class HTML {
|
|
12
|
+
/** @type {SunEditor.Deps} */
|
|
12
13
|
#$;
|
|
13
14
|
#store;
|
|
14
15
|
|
|
@@ -74,6 +75,18 @@ class HTML {
|
|
|
74
75
|
splitTagStyles[n] += tagStyles[k];
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
|
|
79
|
+
// A tag with an explicit entry that is also a line/text-style tag inherits the category
|
|
80
|
+
// styles ('@line'/'@text'), so category edits stay effective for it. (e.g. "li")
|
|
81
|
+
const formatLineReg = options.get('formatLine').reg;
|
|
82
|
+
for (const k in splitTagStyles) {
|
|
83
|
+
if (k.startsWith('@')) continue;
|
|
84
|
+
const category = formatLineReg.test(k) ? '@line' : this.#textStyleTags.includes(k) ? '@text' : '';
|
|
85
|
+
if (category && tagStyles[category]) {
|
|
86
|
+
splitTagStyles[k] += (splitTagStyles[k] ? '|' : '') + tagStyles[category];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
for (const k in splitTagStyles) {
|
|
78
91
|
splitTagStyles[k] = new RegExp(`\\s*[^-a-zA-Z](${splitTagStyles[k]})\\s*:[^;]+(?!;)*`, 'gi');
|
|
79
92
|
}
|
|
@@ -1661,7 +1674,7 @@ class HTML {
|
|
|
1661
1674
|
) || [];
|
|
1662
1675
|
for (let i = ch.length - 1, c; i >= 0; i--) {
|
|
1663
1676
|
c = /** @type {HTMLElement} */ (ch[i]);
|
|
1664
|
-
c.
|
|
1677
|
+
c.replaceWith(...c.childNodes);
|
|
1665
1678
|
}
|
|
1666
1679
|
|
|
1667
1680
|
if (
|
|
@@ -1892,7 +1905,20 @@ class HTML {
|
|
|
1892
1905
|
}
|
|
1893
1906
|
checkTags.push(t);
|
|
1894
1907
|
} else {
|
|
1895
|
-
p.
|
|
1908
|
+
const ref = p.nextSibling;
|
|
1909
|
+
|
|
1910
|
+
let rest = null;
|
|
1911
|
+
if (t.nextSibling) {
|
|
1912
|
+
rest = p.cloneNode(false);
|
|
1913
|
+
while (t.nextSibling) rest.appendChild(t.nextSibling);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
p.parentNode.insertBefore(t, ref);
|
|
1917
|
+
|
|
1918
|
+
if (rest) {
|
|
1919
|
+
p.parentNode.insertBefore(rest, ref);
|
|
1920
|
+
checkTags.push(rest);
|
|
1921
|
+
}
|
|
1896
1922
|
checkTags.push(p);
|
|
1897
1923
|
}
|
|
1898
1924
|
}
|
|
@@ -1929,19 +1955,42 @@ class HTML {
|
|
|
1929
1955
|
}
|
|
1930
1956
|
}
|
|
1931
1957
|
|
|
1958
|
+
// wrap top-level `li` elements without a list parent in a `ul`
|
|
1959
|
+
if (formatFilter) {
|
|
1960
|
+
const orphanCells = dom.query.getListChildNodes(
|
|
1961
|
+
documentFragment,
|
|
1962
|
+
(current) => dom.check.isListCell(current) && !dom.check.isList(current.parentNode),
|
|
1963
|
+
null,
|
|
1964
|
+
);
|
|
1965
|
+
|
|
1966
|
+
for (let i = 0, len = orphanCells.length, t, ul, n, next; i < len; i++) {
|
|
1967
|
+
t = orphanCells[i];
|
|
1968
|
+
if (dom.check.isList(t.parentNode)) continue;
|
|
1969
|
+
|
|
1970
|
+
ul = dom.utils.createElement('UL');
|
|
1971
|
+
t.parentNode.insertBefore(ul, t);
|
|
1972
|
+
n = t;
|
|
1973
|
+
while (n && (dom.check.isListCell(n) || (n.nodeType === 3 && !n.textContent.trim()))) {
|
|
1974
|
+
next = n.nextSibling;
|
|
1975
|
+
ul.appendChild(n);
|
|
1976
|
+
n = next;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1932
1981
|
for (let i = 0, len = withoutFormatCells.length, t, f; i < len; i++) {
|
|
1933
1982
|
t = withoutFormatCells[i];
|
|
1934
1983
|
|
|
1935
1984
|
f = dom.utils.createElement('DIV');
|
|
1936
|
-
f.
|
|
1985
|
+
while (t.firstChild) f.appendChild(t.firstChild);
|
|
1937
1986
|
|
|
1938
|
-
if (
|
|
1987
|
+
if (f.textContent.trim().length === 0 && this.#isAllTextStyleNodes(f)) {
|
|
1939
1988
|
let leaf = /** @type {Element} */ (f);
|
|
1940
1989
|
while (leaf.firstElementChild) leaf = leaf.firstElementChild;
|
|
1941
1990
|
leaf.innerHTML = '<br>';
|
|
1942
1991
|
}
|
|
1943
1992
|
|
|
1944
|
-
t.
|
|
1993
|
+
t.appendChild(f);
|
|
1945
1994
|
}
|
|
1946
1995
|
}
|
|
1947
1996
|
|
|
@@ -106,8 +106,9 @@ class NodeTransform {
|
|
|
106
106
|
|
|
107
107
|
if (temp) {
|
|
108
108
|
if (dom.check.isListCell(newEl) && dom.check.isList(temp) && temp.firstElementChild) {
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
const firstCell = temp.firstElementChild;
|
|
110
|
+
while (firstCell.firstChild) newEl.appendChild(firstCell.firstChild);
|
|
111
|
+
dom.utils.removeItem(firstCell);
|
|
111
112
|
if (temp.children.length > 0) newEl.appendChild(temp);
|
|
112
113
|
} else {
|
|
113
114
|
newEl.appendChild(temp);
|
|
@@ -300,7 +301,8 @@ class NodeTransform {
|
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
303
|
} else {
|
|
303
|
-
child.
|
|
304
|
+
while (next.firstChild) child.appendChild(next.firstChild);
|
|
305
|
+
child.normalize();
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
dom.utils.removeItem(next);
|
|
@@ -959,7 +959,12 @@ class UIManager {
|
|
|
959
959
|
this._updatePlaceholder(fc);
|
|
960
960
|
// document type page
|
|
961
961
|
if (fc.has('documentType_use_page')) {
|
|
962
|
-
|
|
962
|
+
const mirror = fc.get('documentTypePageMirror');
|
|
963
|
+
const frag = mirror.ownerDocument.createDocumentFragment();
|
|
964
|
+
for (let n = fc.get('wysiwyg').firstChild; n; n = n.nextSibling) {
|
|
965
|
+
frag.appendChild(n.cloneNode(true));
|
|
966
|
+
}
|
|
967
|
+
mirror.replaceChildren(frag);
|
|
963
968
|
fc.get('documentType').rePage(true);
|
|
964
969
|
}
|
|
965
970
|
}
|