suneditor 3.2.6 → 3.3.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.
- package/README.md +18 -1
- package/dist/suneditor-contents.min.css +1 -1
- package/dist/suneditor.min.css +3 -3
- package/dist/suneditor.min.js +1 -1
- package/package.json +4 -4
- package/src/assets/design/color.css +5 -2
- package/src/assets/design/size.css +6 -2
- package/src/assets/icons/defaultIcons.js +195 -162
- package/src/assets/suneditor.css +5101 -5105
- package/src/core/config/eventManager.js +24 -1
- package/src/core/config/optionProvider.js +5 -4
- package/src/core/event/actions/index.js +8 -4
- package/src/core/event/effects/keydown.registry.js +59 -10
- package/src/core/event/ports.js +2 -0
- package/src/core/event/rules/keydown.rule.backspace.js +52 -1
- package/src/core/event/rules/keydown.rule.delete.js +51 -0
- package/src/core/logic/dom/format.js +1 -1
- package/src/core/logic/dom/html.js +31 -0
- package/src/core/logic/panel/blockHandle.js +61 -24
- package/src/core/logic/panel/blockResolver.js +41 -4
- package/src/core/logic/panel/finder.js +3 -0
- package/src/core/logic/panel/menu.js +38 -1
- package/src/core/logic/panel/toolbar.js +1 -0
- package/src/core/logic/shell/_commandExecutor.js +2 -1
- package/src/core/logic/shell/pluginManager.js +15 -2
- package/src/core/logic/shell/ui.js +73 -32
- package/src/core/schema/frameContext.js +1 -1
- package/src/core/schema/options.js +49 -3
- package/src/core/section/constructor.js +17 -9
- package/src/modules/contract/Controller.js +14 -0
- package/src/modules/contract/Figure.js +5 -2
- package/src/modules/ui/CommandMenu.js +53 -14
- package/src/modules/ui/SelectMenu.js +103 -36
- package/src/plugins/dropdown/table/index.js +25 -8
- package/src/plugins/dropdown/table/services/table.cell.js +4 -7
- package/src/plugins/dropdown/table/shared/table.constants.js +2 -0
- package/src/plugins/field/slashCommand.js +59 -12
- package/types/assets/icons/defaultIcons.d.ts +1 -0
- package/types/core/event/actions/index.d.ts +1 -0
- package/types/core/event/effects/keydown.registry.d.ts +4 -0
- package/types/core/event/ports.d.ts +3 -0
- package/types/core/logic/panel/blockHandle.d.ts +3 -16
- package/types/core/logic/panel/menu.d.ts +8 -0
- package/types/core/logic/shell/ui.d.ts +31 -5
- package/types/core/schema/frameContext.d.ts +6 -2
- package/types/core/schema/options.d.ts +95 -4
- package/types/modules/ui/CommandMenu.d.ts +11 -0
- package/types/modules/ui/SelectMenu.d.ts +13 -7
- package/types/plugins/dropdown/table/index.d.ts +11 -0
- package/types/plugins/dropdown/table/shared/table.constants.d.ts +1 -0
- package/types/plugins/field/slashCommand.d.ts +43 -2
|
@@ -20,6 +20,9 @@ class EventManager {
|
|
|
20
20
|
/** @type {Array<*>} */
|
|
21
21
|
#events = [];
|
|
22
22
|
|
|
23
|
+
/** @type {?Array<SunEditor.Event.GlobalInfo>} */
|
|
24
|
+
#globalEvents = [];
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* @constructor
|
|
25
28
|
* @param {import('./contextProvider').default} contextProvider
|
|
@@ -165,11 +168,16 @@ class EventManager {
|
|
|
165
168
|
this.#frameContext.get('_ww').addEventListener(type, listener, useCapture);
|
|
166
169
|
}
|
|
167
170
|
_w.addEventListener(type, listener, useCapture);
|
|
168
|
-
|
|
171
|
+
|
|
172
|
+
const info = {
|
|
169
173
|
type,
|
|
170
174
|
listener,
|
|
171
175
|
useCapture,
|
|
172
176
|
};
|
|
177
|
+
|
|
178
|
+
this.#globalEvents?.push(info);
|
|
179
|
+
|
|
180
|
+
return info;
|
|
173
181
|
}
|
|
174
182
|
|
|
175
183
|
/**
|
|
@@ -193,6 +201,12 @@ class EventManager {
|
|
|
193
201
|
}
|
|
194
202
|
_w.removeEventListener(type, listener, useCapture);
|
|
195
203
|
|
|
204
|
+
const i =
|
|
205
|
+
this.#globalEvents?.findIndex(
|
|
206
|
+
(e) => e.type === type && e.listener === listener && e.useCapture === useCapture,
|
|
207
|
+
) ?? -1;
|
|
208
|
+
if (i > -1) this.#globalEvents.splice(i, 1);
|
|
209
|
+
|
|
196
210
|
return null;
|
|
197
211
|
}
|
|
198
212
|
|
|
@@ -218,6 +232,15 @@ class EventManager {
|
|
|
218
232
|
this.#events = null;
|
|
219
233
|
|
|
220
234
|
this.#geckoActiveEvent &&= this.removeGlobalEvent(this.#geckoActiveEvent);
|
|
235
|
+
|
|
236
|
+
const frameWindow = this.#frameOptions.get('iframe') ? this.#frameContext.get('_ww') : null;
|
|
237
|
+
for (let i = 0, len = this.#globalEvents.length, e; i < len; i++) {
|
|
238
|
+
e = this.#globalEvents[i];
|
|
239
|
+
frameWindow?.removeEventListener(e.type, e.listener, e.useCapture);
|
|
240
|
+
_w.removeEventListener(e.type, e.listener, e.useCapture);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.#globalEvents = null;
|
|
221
244
|
}
|
|
222
245
|
}
|
|
223
246
|
|
|
@@ -229,13 +229,14 @@ export default class OptionProvider {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
if (diff.has('placeholder_line')) {
|
|
232
|
-
const
|
|
233
|
-
fc.set('placeholder_line',
|
|
232
|
+
const phLine = newRootOptions.get('placeholder_line');
|
|
233
|
+
fc.set('placeholder_line', phLine);
|
|
234
234
|
|
|
235
235
|
const marked = fc.get('wysiwyg').querySelector('.se-placeholder-line');
|
|
236
236
|
if (marked) {
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
const phText = ui.resolveLinePlaceholder(marked, phLine);
|
|
238
|
+
if (phText) {
|
|
239
|
+
marked.setAttribute('data-se-placeholder-line', phText);
|
|
239
240
|
} else {
|
|
240
241
|
marked.classList.remove('se-placeholder-line');
|
|
241
242
|
marked.removeAttribute('data-se-placeholder-line');
|
|
@@ -138,7 +138,6 @@ export const A = {
|
|
|
138
138
|
* @returns {Action}
|
|
139
139
|
*/
|
|
140
140
|
backspaceEmptyLineMergePrev: (formatEl, prev) => ({ t: 'backspace.emptyLine.mergePrev', p: { formatEl, prev } }),
|
|
141
|
-
|
|
142
141
|
/**
|
|
143
142
|
* @param {Node} rowEndBr - the `<br>` the caret sits on (end of the empty row)
|
|
144
143
|
* @param {Node} rowStartBr - the `<br>` that starts the empty row (removed to merge it up)
|
|
@@ -148,7 +147,6 @@ export const A = {
|
|
|
148
147
|
t: 'backspace.brline.rowMerge',
|
|
149
148
|
p: { rowEndBr, rowStartBr },
|
|
150
149
|
}),
|
|
151
|
-
|
|
152
150
|
/**
|
|
153
151
|
* @description Soft line break merge (Backspace): the caret sits in the zero-width anchor right after a soft-break `<br>` (a Shift+Enter).
|
|
154
152
|
* @param {Node} zws - the zero-width Text node holding the caret (its `previousSibling` is the `<br>`)
|
|
@@ -187,13 +185,11 @@ export const A = {
|
|
|
187
185
|
* @returns {Action}
|
|
188
186
|
*/
|
|
189
187
|
deleteEmptyLineMergeNext: (formatEl, next) => ({ t: 'delete.emptyLine.mergeNext', p: { formatEl, next } }),
|
|
190
|
-
|
|
191
188
|
/**
|
|
192
189
|
* @param {Node} rowEndBr - the `<br>` the caret sits on (end of the empty row); removed to pull the next row up
|
|
193
190
|
* @returns {Action}
|
|
194
191
|
*/
|
|
195
192
|
deleteBrLineRowMerge: (rowEndBr) => ({ t: 'delete.brline.rowMerge', p: { rowEndBr } }),
|
|
196
|
-
|
|
197
193
|
/**
|
|
198
194
|
* @description Soft line break merge (Delete): the caret sits just before a soft-break `<br>`.
|
|
199
195
|
* @param {Node} br - the soft-break `<br>` to remove
|
|
@@ -201,6 +197,14 @@ export const A = {
|
|
|
201
197
|
*/
|
|
202
198
|
deleteSoftBreakMerge: (br) => ({ t: 'delete.softBreak.merge', p: { br } }),
|
|
203
199
|
|
|
200
|
+
// === line ===
|
|
201
|
+
/**
|
|
202
|
+
* @param {HTMLElement} into - Line that receives the content (caret lands at its original end).
|
|
203
|
+
* @param {HTMLElement} from - Line whose content is moved into `into`, then removed.
|
|
204
|
+
* @returns {Action}
|
|
205
|
+
*/
|
|
206
|
+
mergeLineInto: (into, from) => ({ t: 'line.merge', p: { into, from } }),
|
|
207
|
+
|
|
204
208
|
// === tab ===
|
|
205
209
|
/**
|
|
206
210
|
* @param {Range} range
|
|
@@ -250,6 +250,39 @@ export default {
|
|
|
250
250
|
caretToLineEdge(ports, next, false);
|
|
251
251
|
},
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* @action mergeLineInto — Merge one line into another across a block boundary.
|
|
255
|
+
*/
|
|
256
|
+
'line.merge': ({ ports }, { into, from }) => {
|
|
257
|
+
const format = ports.format;
|
|
258
|
+
const container = from.parentElement;
|
|
259
|
+
|
|
260
|
+
stripTrailingBreaks(into);
|
|
261
|
+
const focusNode = into.lastChild;
|
|
262
|
+
stripTrailingBreaks(from);
|
|
263
|
+
|
|
264
|
+
while (from.firstChild) into.appendChild(from.firstChild);
|
|
265
|
+
dom.utils.removeItem(from);
|
|
266
|
+
|
|
267
|
+
let block = container;
|
|
268
|
+
while (
|
|
269
|
+
block &&
|
|
270
|
+
block !== into.parentElement &&
|
|
271
|
+
format.isBlock(block) &&
|
|
272
|
+
!format.isClosureBlock(block) &&
|
|
273
|
+
dom.check.isZeroWidth(block)
|
|
274
|
+
) {
|
|
275
|
+
const parent = block.parentElement;
|
|
276
|
+
dom.utils.removeItem(block);
|
|
277
|
+
block = parent;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!into.firstChild) into.appendChild(dom.utils.createElement('BR'));
|
|
281
|
+
|
|
282
|
+
if (focusNode) caretToNodeEnd(ports, focusNode);
|
|
283
|
+
else ports.selection.setRange(into, 0, into, 0);
|
|
284
|
+
},
|
|
285
|
+
|
|
253
286
|
/** @action deleteBrLineRowMerge — remove an empty row inside a brLine (PRE), pull the next row up */
|
|
254
287
|
'delete.brline.rowMerge': ({ ports }, { rowEndBr }) => {
|
|
255
288
|
let next = rowEndBr.nextSibling;
|
|
@@ -462,7 +495,8 @@ export default {
|
|
|
462
495
|
shouldDelete: true,
|
|
463
496
|
skipHistory: true,
|
|
464
497
|
});
|
|
465
|
-
|
|
498
|
+
|
|
499
|
+
edge.cc.insertBefore(newEl, edge.sc ? edge.sc.nextSibling : edge.cc.firstChild);
|
|
466
500
|
}
|
|
467
501
|
|
|
468
502
|
newEl.innerHTML = '<br>';
|
|
@@ -596,15 +630,17 @@ export default {
|
|
|
596
630
|
dom.utils.copyTagAttributes(newFormat, formatEl, ctx.options.get('lineAttrReset'));
|
|
597
631
|
|
|
598
632
|
let child = focusBR;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
633
|
+
if (!(formatEndEdge && ctx.options.get('lineBreakClearStyle'))) {
|
|
634
|
+
let sNode = selectionNode;
|
|
635
|
+
do {
|
|
636
|
+
if (!dom.check.isBreak(sNode) && sNode.nodeType === 1) {
|
|
637
|
+
const f = /** @type {HTMLElement} */ (sNode.cloneNode(false));
|
|
638
|
+
f.appendChild(child);
|
|
639
|
+
child = f;
|
|
640
|
+
}
|
|
641
|
+
sNode = sNode.parentElement;
|
|
642
|
+
} while (formatEl !== sNode && formatEl.contains(sNode));
|
|
643
|
+
}
|
|
608
644
|
|
|
609
645
|
newFormat.appendChild(child);
|
|
610
646
|
formatEl.parentNode.insertBefore(
|
|
@@ -755,6 +791,19 @@ function caretToNodeEnd(ports, node) {
|
|
|
755
791
|
ports.selection.setRange(node, offset, node, offset);
|
|
756
792
|
}
|
|
757
793
|
|
|
794
|
+
/**
|
|
795
|
+
* @description Removes trailing `<br>` elements and zero-width text nodes from `el`.
|
|
796
|
+
* @param {HTMLElement} el
|
|
797
|
+
*/
|
|
798
|
+
function stripTrailingBreaks(el) {
|
|
799
|
+
let last = el.lastChild;
|
|
800
|
+
while (last && (dom.check.isBreak(last) || (last.nodeType === 3 && dom.check.isZeroWidth(last)))) {
|
|
801
|
+
const prev = last.previousSibling;
|
|
802
|
+
dom.utils.removeItem(last);
|
|
803
|
+
last = prev;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
758
807
|
/**
|
|
759
808
|
* @param {HTMLElement} formatEl - Format element
|
|
760
809
|
* @returns {Node}
|
package/src/core/event/ports.js
CHANGED
|
@@ -34,6 +34,7 @@ import { useEnterFromBeforeInput } from './reducers/keydown.reducer';
|
|
|
34
34
|
* @property {(...args: Parameters<Format['isNormalLine']>) => ReturnType<Format['isNormalLine']>} isNormalLine
|
|
35
35
|
* @property {(...args: Parameters<Format['isBrLine']>) => ReturnType<Format['isBrLine']>} isBrLine
|
|
36
36
|
* @property {(...args: Parameters<Format['isClosureBrLine']>) => ReturnType<Format['isClosureBrLine']>} isClosureBrLine
|
|
37
|
+
* @property {(...args: Parameters<Format['isBlock']>) => ReturnType<Format['isBlock']>} isBlock
|
|
37
38
|
* @property {(...args: Parameters<Format['isClosureBlock']>) => ReturnType<Format['isClosureBlock']>} isClosureBlock
|
|
38
39
|
* @property {(...args: Parameters<Format['isEdgeLine']>) => ReturnType<Format['isEdgeLine']>} isEdgeLine
|
|
39
40
|
* @property {(...args: Parameters<Format['removeBlock']>) => ReturnType<Format['removeBlock']>} removeBlock
|
|
@@ -124,6 +125,7 @@ export function makePorts(inst, { _styleNodes }) {
|
|
|
124
125
|
isNormalLine: (n) => format.isNormalLine(n),
|
|
125
126
|
isBrLine: (n) => format.isBrLine(n),
|
|
126
127
|
isClosureBrLine: (n) => format.isClosureBrLine(n),
|
|
128
|
+
isBlock: (n) => format.isBlock(n),
|
|
127
129
|
isClosureBlock: (n) => format.isClosureBlock(n),
|
|
128
130
|
isEdgeLine: (node, offset, dir) => format.isEdgeLine(node, offset, dir),
|
|
129
131
|
removeBlock: (n, p) => format.removeBlock(n, p),
|
|
@@ -71,6 +71,23 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
if (
|
|
75
|
+
!selectRange &&
|
|
76
|
+
!bidiNotFront &&
|
|
77
|
+
formatEl &&
|
|
78
|
+
format.isNormalLine(formatEl) &&
|
|
79
|
+
!dom.check.isListCell(formatEl) &&
|
|
80
|
+
format.isEdgeLine(range.startContainer, range.startOffset, 'front')
|
|
81
|
+
) {
|
|
82
|
+
const prevLine = findPrevMergeLine(format, formatEl);
|
|
83
|
+
if (prevLine) {
|
|
84
|
+
actions.push(A.preventStop());
|
|
85
|
+
actions.push(A.mergeLineInto(prevLine, formatEl));
|
|
86
|
+
actions.push(A.historyPush(true));
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
// closure, default
|
|
75
92
|
if (
|
|
76
93
|
!selectRange &&
|
|
@@ -84,7 +101,6 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
84
101
|
format.isClosureBrLine(formatEl) ||
|
|
85
102
|
dom.check.isWysiwygFrame(formatEl.parentNode))
|
|
86
103
|
) {
|
|
87
|
-
// closure range
|
|
88
104
|
if (format.isClosureBlock(formatEl.parentNode)) {
|
|
89
105
|
actions.push(A.preventStop());
|
|
90
106
|
return false;
|
|
@@ -348,3 +364,38 @@ export function reduceBackspaceDown(actions, ports, ctx) {
|
|
|
348
364
|
actions.push(A.caretScrollTo(range));
|
|
349
365
|
return true;
|
|
350
366
|
}
|
|
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
|
+
}
|
|
@@ -37,6 +37,23 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
37
37
|
return true;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
if (
|
|
41
|
+
!selectRange &&
|
|
42
|
+
!bidiNotEnd &&
|
|
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) {
|
|
50
|
+
actions.push(A.preventStop());
|
|
51
|
+
actions.push(A.mergeLineInto(formatEl, targetLine));
|
|
52
|
+
actions.push(A.historyPush(true));
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
if (
|
|
41
58
|
!selectRange &&
|
|
42
59
|
!bidiNotEnd &&
|
|
@@ -216,3 +233,37 @@ export function reduceDeleteDown(actions, ports, ctx) {
|
|
|
216
233
|
|
|
217
234
|
return true;
|
|
218
235
|
}
|
|
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
|
+
}
|
|
@@ -290,7 +290,7 @@ class Format {
|
|
|
290
290
|
const lineAttrReset = this.#options.get('lineAttrReset');
|
|
291
291
|
let newEl;
|
|
292
292
|
|
|
293
|
-
if (/^H[1-6]
|
|
293
|
+
if (/^H[1-6]$/i.test(tag) || this.#$.component.is(element)) {
|
|
294
294
|
newEl = dom.utils.createElement(this.#options.get('defaultLine'), null, '<br>');
|
|
295
295
|
} else if (this.isBrLine(element)) {
|
|
296
296
|
newEl = dom.utils.createElement(this.#options.get('defaultLine'), null, '<br>');
|
|
@@ -33,6 +33,7 @@ class HTML {
|
|
|
33
33
|
#disallowedTagsRegExp;
|
|
34
34
|
#disallowedTagNameRegExp;
|
|
35
35
|
#allowedTagNameRegExp;
|
|
36
|
+
#emptyLineRegExp;
|
|
36
37
|
|
|
37
38
|
/** @type {Object<string, RegExp>} */
|
|
38
39
|
#attributeWhitelist;
|
|
@@ -97,6 +98,12 @@ class HTML {
|
|
|
97
98
|
this.#disallowedTagNameRegExp = new RegExp(`^(${disallowedExtraTags})$`, 'i');
|
|
98
99
|
this.#allowedTagNameRegExp = new RegExp(`^(${allowedExtraTags})$`, 'i');
|
|
99
100
|
|
|
101
|
+
// empty default line probe
|
|
102
|
+
this.#emptyLineRegExp = new RegExp(
|
|
103
|
+
`<${options.get('defaultLine')}(?:\\s[^>]*)?></${options.get('defaultLine')}>`,
|
|
104
|
+
'i',
|
|
105
|
+
);
|
|
106
|
+
|
|
100
107
|
// set disallow text nodes
|
|
101
108
|
const disallowStyleNodes = Object.keys(options.get('_defaultStyleTagMap'));
|
|
102
109
|
const allowStyleNodes = !options.get('elementWhitelist')
|
|
@@ -374,6 +381,8 @@ class HTML {
|
|
|
374
381
|
});
|
|
375
382
|
}
|
|
376
383
|
|
|
384
|
+
if (formatFilter && !_freeCodeViewMode) cleanData = this.#dropEmptyLines(cleanData);
|
|
385
|
+
|
|
377
386
|
return cleanData;
|
|
378
387
|
}
|
|
379
388
|
|
|
@@ -1699,6 +1708,28 @@ class HTML {
|
|
|
1699
1708
|
return '';
|
|
1700
1709
|
}
|
|
1701
1710
|
|
|
1711
|
+
/**
|
|
1712
|
+
* @description Drops empty default lines (`<p></p>`) from a cleaned HTML string.
|
|
1713
|
+
* Wrapping block-level content in a default line is invalid HTML, so the parser tears the line
|
|
1714
|
+
* apart and leaves caret-less debris behind. An intentionally blank line always carries `<br>`.
|
|
1715
|
+
* @param {string} html Cleaned HTML string
|
|
1716
|
+
* @returns {string} The string without empty default lines
|
|
1717
|
+
*/
|
|
1718
|
+
#dropEmptyLines(html) {
|
|
1719
|
+
if (!this.#emptyLineRegExp.test(html)) return html;
|
|
1720
|
+
|
|
1721
|
+
const holder = dom.utils.createElement('DIV', null, html);
|
|
1722
|
+
const lines = holder.querySelectorAll(this.#options.get('defaultLine'));
|
|
1723
|
+
let removed = false;
|
|
1724
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1725
|
+
if (lines[i].firstChild) continue;
|
|
1726
|
+
dom.utils.removeItem(lines[i]);
|
|
1727
|
+
removed = true;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
return removed ? holder.innerHTML : html;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1702
1733
|
/**
|
|
1703
1734
|
* @description Checks whether a node is a block-level container in which whitespace-only text
|
|
1704
1735
|
* children are insignificant formatting whitespace (safe to drop), as opposed to an inline/line
|
|
@@ -37,6 +37,10 @@ class BlockHandle {
|
|
|
37
37
|
#plusBtn;
|
|
38
38
|
#dragBtn;
|
|
39
39
|
#menuConfig;
|
|
40
|
+
#menuMaxHeight;
|
|
41
|
+
#menuMinWidth;
|
|
42
|
+
/** @type {?function(SunEditor.Deps, { block: HTMLElement, openMenu: function(): void }): void} */
|
|
43
|
+
#onPlusClickHook;
|
|
40
44
|
|
|
41
45
|
/** @type {CommandMenu|null} */
|
|
42
46
|
#actionMenu = null;
|
|
@@ -74,17 +78,21 @@ class BlockHandle {
|
|
|
74
78
|
* @param {HTMLElement} blockHandle - Handle group (.se-block-handle)
|
|
75
79
|
* @param {HTMLElement} blockHandlePlus - Plus button
|
|
76
80
|
* @param {HTMLElement} blockHandleDrag - Drag button
|
|
77
|
-
* @param {Array
|
|
78
|
-
*
|
|
79
|
-
* define a custom row whose `action` is invoked with the Deps bag and the current block element.
|
|
81
|
+
* @param {Object|Array<*>|null} blockHandleOptions - The `blockHandle` option object (`{ menu, onPlusClick, maxHeight, minWidth }`).
|
|
82
|
+
* - An array is accepted as a shorthand for `{ menu: [...] }`.
|
|
80
83
|
*/
|
|
81
|
-
constructor($, blockHandleArea, blockHandle, blockHandlePlus, blockHandleDrag,
|
|
84
|
+
constructor($, blockHandleArea, blockHandle, blockHandlePlus, blockHandleDrag, blockHandleOptions) {
|
|
82
85
|
this.#$ = $;
|
|
83
86
|
this.#area = blockHandleArea;
|
|
84
87
|
this.#handle = blockHandle;
|
|
85
88
|
this.#plusBtn = blockHandlePlus;
|
|
86
89
|
this.#dragBtn = blockHandleDrag;
|
|
87
|
-
|
|
90
|
+
|
|
91
|
+
const opts = Array.isArray(blockHandleOptions) ? { menu: blockHandleOptions } : blockHandleOptions || {};
|
|
92
|
+
this.#menuConfig = opts.menu || null;
|
|
93
|
+
this.#menuMaxHeight = typeof opts.maxHeight === 'string' ? opts.maxHeight : '';
|
|
94
|
+
this.#menuMinWidth = typeof opts.minWidth === 'string' ? opts.minWidth : '200px';
|
|
95
|
+
this.#onPlusClickHook = typeof opts.onPlusClick === 'function' ? opts.onPlusClick : null;
|
|
88
96
|
|
|
89
97
|
this.#$.contextProvider.carrierWrapper.appendChild(this.#handle);
|
|
90
98
|
|
|
@@ -579,6 +587,7 @@ class BlockHandle {
|
|
|
579
587
|
/**
|
|
580
588
|
* @description Plus button click — insert new line after current block.
|
|
581
589
|
* Mirrors Enter-at-end-of-line behavior from keydown.rule.enter.
|
|
590
|
+
* Adding the line is the fixed behavior; `onPlusClick` decides what happens next (nothing by default).
|
|
582
591
|
* @param {MouseEvent} e
|
|
583
592
|
*/
|
|
584
593
|
#onPlusClick(e) {
|
|
@@ -588,10 +597,20 @@ class BlockHandle {
|
|
|
588
597
|
if (!this.#currentBlock) return;
|
|
589
598
|
|
|
590
599
|
const newLine = this.#$.format.addLineAfter(this.#currentBlock);
|
|
591
|
-
if (newLine)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
600
|
+
if (!newLine) return;
|
|
601
|
+
|
|
602
|
+
this.#$.selection.setRange(newLine, 1, newLine, 1);
|
|
603
|
+
this.#$.history.push(false);
|
|
604
|
+
|
|
605
|
+
if (!this.#onPlusClickHook) return;
|
|
606
|
+
|
|
607
|
+
this.#setCurrentBlock(newLine);
|
|
608
|
+
this.#updatePosition(newLine);
|
|
609
|
+
|
|
610
|
+
this.#onPlusClickHook(this.#$, {
|
|
611
|
+
block: newLine,
|
|
612
|
+
openMenu: () => this.#toggleActionMenu(),
|
|
613
|
+
});
|
|
595
614
|
}
|
|
596
615
|
|
|
597
616
|
/**
|
|
@@ -777,6 +796,22 @@ class BlockHandle {
|
|
|
777
796
|
// Skip if this click was actually a drag
|
|
778
797
|
if (this.#isDragging) return;
|
|
779
798
|
|
|
799
|
+
const componentInfo = this.#$.component.get(this.#currentBlock);
|
|
800
|
+
if (componentInfo) {
|
|
801
|
+
this.#actionMenu?.close();
|
|
802
|
+
this.#clearHoverLines();
|
|
803
|
+
this.#$.component.select(componentInfo.target, componentInfo.pluginName);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
this.#toggleActionMenu();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* @description Open the block action menu (or close it when already open). Shared by the drag button
|
|
812
|
+
* and by the `openMenu` helper handed to the `onPlusClick` hook.
|
|
813
|
+
*/
|
|
814
|
+
#toggleActionMenu() {
|
|
780
815
|
if (!this.#menuConfig) return;
|
|
781
816
|
|
|
782
817
|
// Lazy build — plugins are not yet instantiated when BlockHandle is constructed
|
|
@@ -786,23 +821,24 @@ class BlockHandle {
|
|
|
786
821
|
|
|
787
822
|
if (this.#actionMenu.isOpen) {
|
|
788
823
|
this.#actionMenu.close();
|
|
789
|
-
|
|
790
|
-
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
791
826
|
|
|
792
|
-
|
|
793
|
-
const lines = this.#$.format.getLines(null);
|
|
794
|
-
if (lines.length > 0) {
|
|
795
|
-
this.#setHoverLines(lines);
|
|
796
|
-
}
|
|
827
|
+
this.#expandRangeToFullLines();
|
|
797
828
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const horiz = this.#$.options.get('_rtl') ? 'left' : 'right';
|
|
803
|
-
const dir = `${horiz}-${spaceBelow >= spaceAbove ? 'bottom' : 'top'}`;
|
|
804
|
-
this.#actionMenu.open(dir);
|
|
829
|
+
// Highlight selected range lines
|
|
830
|
+
const lines = this.#$.format.getLines(null);
|
|
831
|
+
if (lines.length > 0) {
|
|
832
|
+
this.#setHoverLines(lines);
|
|
805
833
|
}
|
|
834
|
+
|
|
835
|
+
// Choose open direction based on available space.
|
|
836
|
+
const btnGlobal = this.#$.offset.getGlobal(this.#dragBtn);
|
|
837
|
+
const spaceBelow = dom.utils.getClientSize().h - (btnGlobal.top - _w.scrollY + btnGlobal.height);
|
|
838
|
+
const spaceAbove = btnGlobal.top - _w.scrollY;
|
|
839
|
+
const horiz = this.#$.options.get('_rtl') ? 'left' : 'right';
|
|
840
|
+
const dir = `${horiz}-${spaceBelow >= spaceAbove ? 'bottom' : 'top'}`;
|
|
841
|
+
this.#actionMenu.open(dir);
|
|
806
842
|
}
|
|
807
843
|
|
|
808
844
|
/**
|
|
@@ -816,7 +852,8 @@ class BlockHandle {
|
|
|
816
852
|
selectMenuParams: {
|
|
817
853
|
position: 'right-top',
|
|
818
854
|
dir: this.#$.options.get('_rtl') ? 'rtl' : 'ltr',
|
|
819
|
-
minWidth:
|
|
855
|
+
minWidth: this.#menuMinWidth,
|
|
856
|
+
maxHeight: this.#menuMaxHeight,
|
|
820
857
|
keydownTarget: _w,
|
|
821
858
|
closeMethod: () => {
|
|
822
859
|
dom.utils.removeClass(this.#dragBtn, 'on');
|
|
@@ -35,6 +35,7 @@ const TABLE_INNER_RE = /^(THEAD|TBODY|TR|TD|TH)$/;
|
|
|
35
35
|
*/
|
|
36
36
|
function classifyType(el) {
|
|
37
37
|
const tag = el.nodeName;
|
|
38
|
+
if (isComponentContainer(el)) return 'component';
|
|
38
39
|
if (tag === 'P' || tag === 'DIV') return 'p';
|
|
39
40
|
if (HEADING_RE.test(tag)) return 'heading';
|
|
40
41
|
if (tag === 'LI') return 'list-item';
|
|
@@ -74,6 +75,24 @@ function isInsideComponent(node) {
|
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @description Resolve a node to its outermost component container, but only when that container sits directly on the wysiwyg root.
|
|
80
|
+
* @param {Node} node
|
|
81
|
+
* @returns {HTMLElement|null} The top-level component container, or `null` if there is none
|
|
82
|
+
*/
|
|
83
|
+
function resolveTopLevelComponent(node) {
|
|
84
|
+
let el = node;
|
|
85
|
+
let outermost = null;
|
|
86
|
+
|
|
87
|
+
while (el && !isWysiwygFrame(el)) {
|
|
88
|
+
if (el.nodeType === 1 && isComponentContainer(/** @type {Element} */ (el)))
|
|
89
|
+
outermost = /** @type {HTMLElement} */ (el);
|
|
90
|
+
el = el.parentNode;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return outermost && outermost.parentNode && isWysiwygFrame(outermost.parentNode) ? outermost : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
/**
|
|
78
97
|
* @description Count block-level ancestors between element and wysiwyg root.
|
|
79
98
|
* @param {HTMLElement} element
|
|
@@ -193,8 +212,10 @@ export function resolveBlock(node, format, wysiwygFrame, mouseY) {
|
|
|
193
212
|
// Already at wysiwyg root
|
|
194
213
|
if (isWysiwygFrame(node)) return null;
|
|
195
214
|
|
|
196
|
-
|
|
197
|
-
|
|
215
|
+
if (isInsideComponent(node)) {
|
|
216
|
+
const component = resolveTopLevelComponent(node);
|
|
217
|
+
return component ? describeBlock(component, format, mouseY) : null;
|
|
218
|
+
}
|
|
198
219
|
|
|
199
220
|
let resolved = null;
|
|
200
221
|
|
|
@@ -248,8 +269,24 @@ export function resolveBlock(node, format, wysiwygFrame, mouseY) {
|
|
|
248
269
|
|
|
249
270
|
if (!resolved) return null;
|
|
250
271
|
|
|
251
|
-
// Final component check on resolved element
|
|
252
|
-
if (isInsideComponent(resolved))
|
|
272
|
+
// Final component check on the resolved element (e.g. getLine walked into a component)
|
|
273
|
+
if (isInsideComponent(resolved)) {
|
|
274
|
+
const component = resolveTopLevelComponent(resolved);
|
|
275
|
+
return component ? describeBlock(component, format, mouseY) : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return describeBlock(resolved, format, mouseY);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @description Build the `BlockInfo` for an already-resolved block element.
|
|
283
|
+
* @param {HTMLElement} element - Resolved block-level element
|
|
284
|
+
* @param {FormatAPI} format - Injected format methods
|
|
285
|
+
* @param {number} [mouseY] - Mouse clientY for nested list resolution
|
|
286
|
+
* @returns {BlockInfo}
|
|
287
|
+
*/
|
|
288
|
+
function describeBlock(element, format, mouseY) {
|
|
289
|
+
let resolved = element;
|
|
253
290
|
|
|
254
291
|
// For UL/OL, resolve to the closest child LI by mouse Y.
|
|
255
292
|
// For LI with nested sub-lists, find the deepest child LI.
|
|
@@ -125,6 +125,8 @@ class Finder {
|
|
|
125
125
|
|
|
126
126
|
this.#isOpen = true;
|
|
127
127
|
|
|
128
|
+
dom.utils.addClass(this.#$.commandDispatcher.targets.get('finder'), 'active');
|
|
129
|
+
|
|
128
130
|
// Listen for wysiwyg content changes to refresh highlights
|
|
129
131
|
this.#addContentInputListener();
|
|
130
132
|
|
|
@@ -175,6 +177,7 @@ class Finder {
|
|
|
175
177
|
if (!this.#isOpen) return;
|
|
176
178
|
|
|
177
179
|
this.#isOpen = false;
|
|
180
|
+
dom.utils.removeClass(this.#$.commandDispatcher.targets.get('finder'), 'active');
|
|
178
181
|
this.#clearHighlights();
|
|
179
182
|
this.#matches = [];
|
|
180
183
|
this.#currentIndex = -1;
|