suneditor 3.2.5 → 3.3.0
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-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 +99 -99
- 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/selection.js +4 -2
- 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/ui.js +41 -5
- package/src/core/schema/frameContext.js +1 -1
- package/src/core/schema/options.js +33 -3
- package/src/core/section/constructor.js +5 -1
- package/src/modules/contract/Controller.js +14 -0
- package/src/modules/ui/CommandMenu.js +8 -11
- package/src/modules/ui/SelectMenu.js +6 -0
- package/src/plugins/dropdown/table/services/table.cell.js +4 -7
- 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/menu.d.ts +8 -0
- package/types/core/logic/shell/ui.d.ts +17 -0
- package/types/core/schema/frameContext.d.ts +6 -2
- package/types/core/schema/options.d.ts +70 -4
|
@@ -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
|
+
}
|
|
@@ -531,8 +531,9 @@ class Selection_ {
|
|
|
531
531
|
if (topMargin >= 0 && bottomMargin >= 0) return;
|
|
532
532
|
|
|
533
533
|
const newScrollTop = scrollY - (topMargin < 0 ? -(topMargin - PADDING) : bottomMargin);
|
|
534
|
+
const clearTopToolbar = topToolbarH > 0 && topMargin < 0;
|
|
534
535
|
_w.scrollTo({
|
|
535
|
-
top:
|
|
536
|
+
top: clearTopToolbar ? newScrollTop - topToolbarH : newScrollTop,
|
|
536
537
|
behavior,
|
|
537
538
|
});
|
|
538
539
|
} else {
|
|
@@ -549,8 +550,9 @@ class Selection_ {
|
|
|
549
550
|
scrollMargin <= PADDING
|
|
550
551
|
? scrollY - scrollMargin + PADDING + statusbarHeight
|
|
551
552
|
: scrollY - scrollMargin + (viewHeight - elH - PADDING);
|
|
553
|
+
const clearTopToolbar = topToolbarH > 0 && !topClear;
|
|
552
554
|
_w.scrollTo({
|
|
553
|
-
top:
|
|
555
|
+
top: clearTopToolbar ? newScrollTop - topToolbarH : newScrollTop,
|
|
554
556
|
behavior,
|
|
555
557
|
});
|
|
556
558
|
}
|
|
@@ -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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dom, converter, env } from '../../../helper';
|
|
1
|
+
import { dom, converter, env, keyCodeMap } from '../../../helper';
|
|
2
2
|
|
|
3
3
|
const { isMobile, _w } = env;
|
|
4
4
|
|
|
@@ -19,6 +19,7 @@ class Menu {
|
|
|
19
19
|
#dropdownCommands = [];
|
|
20
20
|
#bindClose_dropdown_mouse = null;
|
|
21
21
|
#bindClose_dropdown_key = null;
|
|
22
|
+
#bindClose_dropdown_esc = null;
|
|
22
23
|
#bindClose_cons_mouse = null;
|
|
23
24
|
#bindMenu_mousemove = null;
|
|
24
25
|
#bindMenu_mouseout = null;
|
|
@@ -27,6 +28,8 @@ class Menu {
|
|
|
27
28
|
#deferredShowTimer = null;
|
|
28
29
|
#viewportListener = null;
|
|
29
30
|
#visualViewport = null;
|
|
31
|
+
/** @type {Set<() => void>} */
|
|
32
|
+
#dropdownOffSubs = new Set();
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* @constructor
|
|
@@ -64,11 +67,13 @@ class Menu {
|
|
|
64
67
|
mousedown: this.#OnMouseDown_dropdown.bind(this),
|
|
65
68
|
containerDown: this.containerOff.bind(this),
|
|
66
69
|
keydown: this.#OnKeyDown_dropdown.bind(this),
|
|
70
|
+
esc: this.#OnKeyDown_dropdown_esc.bind(this),
|
|
67
71
|
mousemove: this.#OnMousemove_dropdown.bind(this),
|
|
68
72
|
mouseout: this.#OnMouseout_dropdown.bind(this),
|
|
69
73
|
};
|
|
70
74
|
this.#bindClose_dropdown_mouse = null;
|
|
71
75
|
this.#bindClose_dropdown_key = null;
|
|
76
|
+
this.#bindClose_dropdown_esc = null;
|
|
72
77
|
this.#bindClose_cons_mouse = null;
|
|
73
78
|
|
|
74
79
|
// eventManager member (viewport)
|
|
@@ -150,6 +155,9 @@ class Menu {
|
|
|
150
155
|
this.#globalEventHandler.mousedown,
|
|
151
156
|
false,
|
|
152
157
|
);
|
|
158
|
+
|
|
159
|
+
this.#bindClose_dropdown_esc = this.#eventManager.addGlobalEvent('keydown', this.#globalEventHandler.esc, true);
|
|
160
|
+
|
|
153
161
|
if (this.#dropdownCommands.includes(dropdownName)) {
|
|
154
162
|
this.menus = converter.nodeListToArray(menu.querySelectorAll('[data-command]'));
|
|
155
163
|
if (this.menus.length > 0) {
|
|
@@ -208,6 +216,20 @@ class Menu {
|
|
|
208
216
|
|
|
209
217
|
this.#store.set('_preventBlur', false);
|
|
210
218
|
this.currentDropdownPlugin = null;
|
|
219
|
+
|
|
220
|
+
for (const cb of [...this.#dropdownOffSubs]) cb();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @description Subscribe to be notified after a dropdown is turned off — i.e. a dropdown-free
|
|
225
|
+
* plugin committed and closed itself via {@link dropdownOff}. Mirrors {@link Store#subscribe}:
|
|
226
|
+
* returns an unsubscribe function.
|
|
227
|
+
* @param {() => void} callback
|
|
228
|
+
* @returns {() => void} Unsubscribe function
|
|
229
|
+
*/
|
|
230
|
+
subscribeDropdownOff(callback) {
|
|
231
|
+
this.#dropdownOffSubs.add(callback);
|
|
232
|
+
return () => this.#dropdownOffSubs.delete(callback);
|
|
211
233
|
}
|
|
212
234
|
|
|
213
235
|
/**
|
|
@@ -424,6 +446,7 @@ class Menu {
|
|
|
424
446
|
*/
|
|
425
447
|
#removeGlobalEvent() {
|
|
426
448
|
this.#bindClose_dropdown_mouse &&= this.#eventManager.removeGlobalEvent(this.#bindClose_dropdown_mouse);
|
|
449
|
+
this.#bindClose_dropdown_esc &&= this.#eventManager.removeGlobalEvent(this.#bindClose_dropdown_esc);
|
|
427
450
|
this.#bindClose_cons_mouse &&= this.#eventManager.removeGlobalEvent(this.#bindClose_cons_mouse);
|
|
428
451
|
if (this.#bindClose_dropdown_key) {
|
|
429
452
|
this.#bindClose_dropdown_key = this.#eventManager.removeGlobalEvent(this.#bindClose_dropdown_key);
|
|
@@ -491,6 +514,20 @@ class Menu {
|
|
|
491
514
|
}
|
|
492
515
|
}
|
|
493
516
|
|
|
517
|
+
/**
|
|
518
|
+
* @description Closes the open dropdown on ESC. Bound as a capture-phase global listener for every dropdown.
|
|
519
|
+
* @param {KeyboardEvent} e - Event object
|
|
520
|
+
*/
|
|
521
|
+
#OnKeyDown_dropdown_esc(e) {
|
|
522
|
+
if (!keyCodeMap.isEsc(e.code)) return;
|
|
523
|
+
if (this.#$.ui.opendControllers?.some(({ form }) => form && dom.utils.hasClass(form, 'se-dropdown'))) return;
|
|
524
|
+
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
e.stopPropagation();
|
|
527
|
+
|
|
528
|
+
this.dropdownOff();
|
|
529
|
+
}
|
|
530
|
+
|
|
494
531
|
/**
|
|
495
532
|
* @param {MouseEvent} e - Event object
|
|
496
533
|
*/
|
|
@@ -93,7 +93,8 @@ export default class CommandExecutor {
|
|
|
93
93
|
}
|
|
94
94
|
break;
|
|
95
95
|
case 'finder':
|
|
96
|
-
this.#$.finder.
|
|
96
|
+
if (this.#$.finder.isOpen) this.#$.finder.close();
|
|
97
|
+
else this.#$.finder.open(true);
|
|
97
98
|
break;
|
|
98
99
|
case 'codeView':
|
|
99
100
|
this.#$.viewer.codeView(!this.#frameContext.get('isCodeView'));
|
|
@@ -873,22 +873,58 @@ class UIManager {
|
|
|
873
873
|
}
|
|
874
874
|
}
|
|
875
875
|
|
|
876
|
+
/**
|
|
877
|
+
* @description Resolves the per-line placeholder text for an empty line from the `placeholder_line` option.
|
|
878
|
+
* - String option: one hint for every empty line — except list cells and table cells (backward compatible).
|
|
879
|
+
* - Object option: keyed by tag name or a category sentinel matching the editor's format classification
|
|
880
|
+
* ({@link Format#isNormalLine}, `isBrLine`, `isClosureBrLine`, `isBlock`, `isClosureBlock`, list cells).
|
|
881
|
+
* @param {?Node} line - The (empty) line element.
|
|
882
|
+
* @param {string|Object<string, string>} opt - The `placeholder_line` option value.
|
|
883
|
+
* @returns {string} The resolved placeholder text (`''` = none).
|
|
884
|
+
*/
|
|
885
|
+
resolveLinePlaceholder(line, opt) {
|
|
886
|
+
if (!line || !opt) return '';
|
|
887
|
+
|
|
888
|
+
if (typeof opt !== 'object') {
|
|
889
|
+
return dom.check.isListCell(line) || dom.query.getParentElement(line, dom.check.isTableCell) ? '' : opt;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const format = this.#$.format;
|
|
893
|
+
const tag = line.nodeName.toLowerCase();
|
|
894
|
+
|
|
895
|
+
if (typeof opt[tag] === 'string') return opt[tag];
|
|
896
|
+
if (dom.check.isListCell(line) && typeof opt['@list'] === 'string') return opt['@list'];
|
|
897
|
+
if (format.isClosureBrLine(line) && typeof opt['@closureBrLine'] === 'string') return opt['@closureBrLine'];
|
|
898
|
+
if (format.isBrLine(line) && typeof opt['@brLine'] === 'string') return opt['@brLine'];
|
|
899
|
+
|
|
900
|
+
const block = format.getBlock(line);
|
|
901
|
+
if (block && block !== line) {
|
|
902
|
+
const blockTag = block.nodeName.toLowerCase();
|
|
903
|
+
if (typeof opt[blockTag] === 'string') return opt[blockTag];
|
|
904
|
+
if (format.isClosureBlock(block) && typeof opt['@closureBlock'] === 'string') return opt['@closureBlock'];
|
|
905
|
+
if (typeof opt['@block'] === 'string') return opt['@block'];
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (format.isNormalLine(line) && typeof opt['@normalLine'] === 'string') return opt['@normalLine'];
|
|
909
|
+
return typeof opt['@line'] === 'string' ? opt['@line'] : '';
|
|
910
|
+
}
|
|
911
|
+
|
|
876
912
|
/**
|
|
877
913
|
* @internal
|
|
878
914
|
* @description Notion-style per-line placeholder. Marks the focused empty line so a CSS `::before`
|
|
879
|
-
* renders the hint text
|
|
915
|
+
* renders the hint text resolved for that line's type (see {@link resolveLinePlaceholder}).
|
|
880
916
|
* @param {SunEditor.FrameContext} fc - Frame context
|
|
881
917
|
* @returns {boolean} Whether the line placeholder is currently shown.
|
|
882
918
|
*/
|
|
883
919
|
#updateLinePlaceholder(fc) {
|
|
884
|
-
const
|
|
885
|
-
if (!
|
|
920
|
+
const opt = fc.get('placeholder_line');
|
|
921
|
+
if (!opt) return false;
|
|
886
922
|
|
|
887
923
|
const wysiwyg = fc.get('wysiwyg');
|
|
888
924
|
|
|
889
925
|
const line = this.#store.get('hasFocus') ? this.#$.format.getLine(this.#$.selection.selectionNode) : null;
|
|
890
|
-
const
|
|
891
|
-
const target =
|
|
926
|
+
const text = dom.check.isEmptyLine(line) ? this.resolveLinePlaceholder(line, opt) : '';
|
|
927
|
+
const target = text ? line : null;
|
|
892
928
|
|
|
893
929
|
const prevMarkers = wysiwyg.querySelectorAll('.se-placeholder-line');
|
|
894
930
|
for (let i = 0; i < prevMarkers.length; i++) {
|
|
@@ -34,7 +34,7 @@ import { get as getNumber } from '../../helper/numbers';
|
|
|
34
34
|
* @property {HTMLTextAreaElement} markdown - Markdown view editing element (a <textarea>).
|
|
35
35
|
* @property {HTMLTextAreaElement} markdownNumbers - Element displaying line numbers in markdown view mode.
|
|
36
36
|
* @property {HTMLElement} placeholder - Placeholder element shown when the editor is empty.
|
|
37
|
-
* @property {string} placeholder_line - Per-line placeholder text, rendered via a `::before` on the focused empty line.
|
|
37
|
+
* @property {string|Object<string, string>} placeholder_line - Per-line placeholder text, rendered via a `::before` on the focused empty line.
|
|
38
38
|
* @property {HTMLElement} statusbar - Editor status bar element (for resizing, info, etc.).
|
|
39
39
|
* @property {HTMLElement} navigation - Navigation element (e.g., for outline or bookmarks).
|
|
40
40
|
* @property {HTMLElement} charWrapper - Wrapper for the character counter element.
|
|
@@ -104,8 +104,22 @@ export const DEFAULTS = {
|
|
|
104
104
|
* === Content & Editing ===
|
|
105
105
|
* @property {string} [value=""] - Initial value for the editor.
|
|
106
106
|
* @property {string} [placeholder=""] - Placeholder text shown when the whole editor is empty.
|
|
107
|
-
* @property {string} [placeholder_line=""] - per-line placeholder shown on the focused
|
|
107
|
+
* @property {string|Object<string, string>} [placeholder_line=""] - per-line placeholder shown on the focused
|
|
108
108
|
* line when that line is empty. Takes priority over `placeholder` while a line is focused.
|
|
109
|
+
* - **string**: one hint for every empty line (list cells and table cells excluded).
|
|
110
|
+
* - **object**: per-type hints keyed by tag name (`p`, `pre`, `blockquote`, ...) or a category sentinel
|
|
111
|
+
* matching the editor's format classification: `@line`, `@normalLine`, `@list`, `@brLine`, `@closureBrLine`,
|
|
112
|
+
* `@block`, `@closureBlock`. Resolved most-specific → least, like `tagStyles`:
|
|
113
|
+
* `<tag>` → `@list` → `@closureBrLine` → `@brLine` → block container (`<blockTag>` → `@closureBlock` → `@block`)
|
|
114
|
+
* → `@normalLine` → `@line`. A missing key = no placeholder for that type; an explicit `''` suppresses it.
|
|
115
|
+
* ```js
|
|
116
|
+
* // one hint everywhere
|
|
117
|
+
* placeholder_line: 'Type something…'
|
|
118
|
+
* // per-type
|
|
119
|
+
* placeholder_line: {
|
|
120
|
+
* '@normalLine': 'Type…', '@list': 'List item', '@block': 'Quote…', '@closureBlock': 'Cell', pre: '// code'
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
109
123
|
* @property {Object<string, string>} [editableFrameAttributes={spellcheck: "false"}] - Attributes for the editable frame[.sun-editor-editable].
|
|
110
124
|
* ```js
|
|
111
125
|
* { editableFrameAttributes: { spellcheck: 'true', autocomplete: 'on' } }
|
|
@@ -460,6 +474,13 @@ export const DEFAULTS = {
|
|
|
460
474
|
* - Formats that include `line`, such as "Quote", still operate on a `line` basis.
|
|
461
475
|
* - suneditor processes work in `line` units.
|
|
462
476
|
* - When set to `br`, performance may decrease when editing a lot of data.
|
|
477
|
+
* @property {boolean} [lineBreakClearStyle=false] - When `true`, pressing Enter at the **end** of a line
|
|
478
|
+
* starts a fresh line that does not carry the caret's inline style nodes (e.g. bold/italic/color spans, links);
|
|
479
|
+
* the line-level element and its attributes are preserved.
|
|
480
|
+
* Only affects the end-of-line case — mid-line splits, start-of-line breaks, and Shift+Enter are unchanged.
|
|
481
|
+
* ```js
|
|
482
|
+
* { lineBreakClearStyle: true }
|
|
483
|
+
* ```
|
|
463
484
|
* @property {string} [lineAttrReset=""] - Line properties that should be reset when changing lines. Delimiter: `"|"`.
|
|
464
485
|
* ```js
|
|
465
486
|
* { lineAttrReset: 'id|name' }
|
|
@@ -510,12 +531,16 @@ export const DEFAULTS = {
|
|
|
510
531
|
* { toolbar_innerWidth: 'auto' }
|
|
511
532
|
* ```
|
|
512
533
|
* @property {?HTMLElement} [toolbar_container] - Container element for the toolbar.
|
|
513
|
-
* @property {number|{top: number, offset
|
|
534
|
+
* @property {number|{top: number, offset?: number, position?: "sticky"|"fixed"}} [toolbar_sticky=0] - Enables sticky toolbar.
|
|
514
535
|
* - `number`: Sets the sticky top position (px). Use `-1` to disable sticky.
|
|
515
536
|
* - `{top, offset}`: `top` is the sticky position when the page header is visible.
|
|
516
537
|
* - `offset` is the sticky position when a virtual keyboard shifts the viewport (e.g., on tablets, touch devices).
|
|
517
538
|
* - When the virtual keyboard is active, `offset` replaces `top` so the toolbar doesn't leave a gap
|
|
518
539
|
* - for a page header that has scrolled out of view. Default `offset` is `0`.
|
|
540
|
+
* - `position` (default `"sticky"`): the positioning engine.
|
|
541
|
+
* - `"sticky"` uses native CSS `position: sticky` (with a JS `position: fixed` fallback where unsupported).
|
|
542
|
+
* `"fixed"` forces the JS `position: fixed` engine
|
|
543
|
+
* - even when CSS sticky is supported — for environments where CSS sticky silently misbehaves and can't be
|
|
519
544
|
* ```js
|
|
520
545
|
* // Basic usage — sticky at top with 0px offset
|
|
521
546
|
* toolbar_sticky: 0
|
|
@@ -525,6 +550,9 @@ export const DEFAULTS = {
|
|
|
525
550
|
*
|
|
526
551
|
* // 92px header on desktop, but 0px when virtual keyboard pushes the viewport
|
|
527
552
|
* toolbar_sticky: { top: 92, offset: 0 }
|
|
553
|
+
*
|
|
554
|
+
* // Force the JS position:fixed engine (CSS sticky unreliable in this environment)
|
|
555
|
+
* toolbar_sticky: { top: 0, position: 'fixed' }
|
|
528
556
|
* ```
|
|
529
557
|
* @property {boolean} [toolbar_hide=false] - Hides toolbar initially.
|
|
530
558
|
* @property {Object} [subToolbar={}] - Sub-toolbar configuration. A secondary toolbar that appears on text selection.
|
|
@@ -776,6 +804,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
776
804
|
printClass: true,
|
|
777
805
|
defaultLine: 'fixed',
|
|
778
806
|
defaultLineBreakFormat: true,
|
|
807
|
+
lineBreakClearStyle: true,
|
|
779
808
|
scopeSelectionTags: true,
|
|
780
809
|
__defaultElementWhitelist: 'fixed',
|
|
781
810
|
elementWhitelist: 'fixed',
|
|
@@ -822,7 +851,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
822
851
|
};
|
|
823
852
|
|
|
824
853
|
/**
|
|
825
|
-
* @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | '_toolbar_sticky' | '_toolbar_sticky_offset' | 'strictMode' | 'lineAttrReset'} TransformedOptionKeys
|
|
854
|
+
* @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | '_toolbar_sticky' | '_toolbar_sticky_offset' | '_toolbar_sticky_fixed' | 'strictMode' | 'lineAttrReset'} TransformedOptionKeys
|
|
826
855
|
*/
|
|
827
856
|
|
|
828
857
|
/**
|
|
@@ -846,6 +875,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
846
875
|
* @property {HTMLElement|null} toolbar_container
|
|
847
876
|
* @property {number} _toolbar_sticky
|
|
848
877
|
* @property {number} _toolbar_sticky_offset
|
|
878
|
+
* @property {boolean} _toolbar_sticky_fixed
|
|
849
879
|
* @property {StrictModeOptions} strictMode
|
|
850
880
|
* @property {string[]} lineAttrReset
|
|
851
881
|
*/
|
|
@@ -89,7 +89,7 @@ function Constructor(editorTargets, options) {
|
|
|
89
89
|
style: 'position: fixed !important; top: -10000px !important; left: -10000px !important; display: block !important; width: 0 !important; height: 0 !important; margin: 0 !important; padding: 0 !important;',
|
|
90
90
|
})
|
|
91
91
|
);
|
|
92
|
-
focusTemp.tabIndex =
|
|
92
|
+
focusTemp.tabIndex = -1;
|
|
93
93
|
editor_carrier_wrapper.appendChild(focusTemp);
|
|
94
94
|
|
|
95
95
|
// modal
|
|
@@ -749,6 +749,7 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
749
749
|
typeof options.defaultLine === 'string' && options.defaultLine.length > 0 ? options.defaultLine : 'p',
|
|
750
750
|
);
|
|
751
751
|
o.set('defaultLineBreakFormat', options.defaultLineBreakFormat || 'line');
|
|
752
|
+
o.set('lineBreakClearStyle', !!options.lineBreakClearStyle);
|
|
752
753
|
o.set('scopeSelectionTags', options.scopeSelectionTags || DEFAULTS.SCOPE_SELECTION_TAGS);
|
|
753
754
|
// element
|
|
754
755
|
const elw = (typeof options.elementWhitelist === 'string' ? options.elementWhitelist : '').toLowerCase();
|
|
@@ -893,12 +894,15 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
893
894
|
if (_isBalloon) {
|
|
894
895
|
o.set('_toolbar_sticky', -1);
|
|
895
896
|
o.set('_toolbar_sticky_offset', 0);
|
|
897
|
+
o.set('_toolbar_sticky_fixed', false);
|
|
896
898
|
} else if (_stickyOpt !== null && typeof _stickyOpt === 'object') {
|
|
897
899
|
o.set('_toolbar_sticky', numbers.get(_stickyOpt.top, 0));
|
|
898
900
|
o.set('_toolbar_sticky_offset', numbers.get(_stickyOpt.offset, 0));
|
|
901
|
+
o.set('_toolbar_sticky_fixed', _stickyOpt.position === 'fixed');
|
|
899
902
|
} else {
|
|
900
903
|
o.set('_toolbar_sticky', _stickyOpt === undefined ? 0 : numbers.is(_stickyOpt) ? _stickyOpt : -1);
|
|
901
904
|
o.set('_toolbar_sticky_offset', 0);
|
|
905
|
+
o.set('_toolbar_sticky_fixed', false);
|
|
902
906
|
}
|
|
903
907
|
|
|
904
908
|
o.set('toolbar_hide', !!options.toolbar_hide);
|
|
@@ -8,6 +8,11 @@ const INDEX_S_1 = '2147483642';
|
|
|
8
8
|
const INDEX_1 = '2147483641';
|
|
9
9
|
const ADD_OFFSET_VALUE = { left: 0, right: 0, top: 0 };
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @type {?Controller}
|
|
13
|
+
*/
|
|
14
|
+
let _topHoverController = null;
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* Controller information object
|
|
13
18
|
* @typedef {Object} ControllerInfo
|
|
@@ -414,6 +419,7 @@ class Controller {
|
|
|
414
419
|
* @description Hide controller at editor area (link button, image resize button..)
|
|
415
420
|
*/
|
|
416
421
|
#controllerOff() {
|
|
422
|
+
if (_topHoverController === this) _topHoverController = null;
|
|
417
423
|
this.form.hidePopover?.();
|
|
418
424
|
this.form.style.display = 'none';
|
|
419
425
|
this.#$.ui.opendControllers = this.#$.ui.opendControllers.filter((v) => v.form !== this.form);
|
|
@@ -586,6 +592,14 @@ class Controller {
|
|
|
586
592
|
|
|
587
593
|
const eventTarget = dom.query.getEventTarget(e);
|
|
588
594
|
eventTarget.style.zIndex = this.toTop ? INDEX_00 : INDEX_0;
|
|
595
|
+
|
|
596
|
+
// The z-index above governs only the no-popover fallback. In the popover top layer, stacking follows
|
|
597
|
+
// show order — so to lift the hovered controller (and its tooltips) above an overlapping sibling, re-show it.
|
|
598
|
+
if (this.sibling && _topHoverController !== this && this.form.matches?.(':popover-open')) {
|
|
599
|
+
_topHoverController = this;
|
|
600
|
+
this.form.hidePopover();
|
|
601
|
+
this.form.showPopover();
|
|
602
|
+
}
|
|
589
603
|
}
|
|
590
604
|
|
|
591
605
|
/**
|
|
@@ -97,7 +97,7 @@ class CommandMenu {
|
|
|
97
97
|
|
|
98
98
|
/** @type {Map<number, { name: string, plugin: any, li: HTMLElement }>} */
|
|
99
99
|
#freeMap = new Map();
|
|
100
|
-
/** @type {?{ dropdown: HTMLElement, plugin: any, originalParent: ?Node, anchorLi: HTMLElement,
|
|
100
|
+
/** @type {?{ dropdown: HTMLElement, plugin: any, originalParent: ?Node, anchorLi: HTMLElement, unsub: () => void }} */
|
|
101
101
|
#flyoutState = null;
|
|
102
102
|
|
|
103
103
|
/**
|
|
@@ -499,16 +499,13 @@ class CommandMenu {
|
|
|
499
499
|
dom.utils.addClass(anchorLi, 'se-submenu-open');
|
|
500
500
|
plugin.on?.(anchorLi);
|
|
501
501
|
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this.selectMenu.close();
|
|
508
|
-
}, 0),
|
|
509
|
-
);
|
|
502
|
+
// dropdown-off event and unsubscribe on close (see `#closeFlyout`) rather than patching core.
|
|
503
|
+
const unsub = this.#$.menu.subscribeDropdownOff(() => {
|
|
504
|
+
this.#closeFlyout();
|
|
505
|
+
this.selectMenu.close();
|
|
506
|
+
});
|
|
510
507
|
|
|
511
|
-
this.#flyoutState = { dropdown, plugin, originalParent, anchorLi,
|
|
508
|
+
this.#flyoutState = { dropdown, plugin, originalParent, anchorLi, unsub };
|
|
512
509
|
}
|
|
513
510
|
|
|
514
511
|
/**
|
|
@@ -520,7 +517,7 @@ class CommandMenu {
|
|
|
520
517
|
if (!s) return;
|
|
521
518
|
this.#flyoutState = null;
|
|
522
519
|
|
|
523
|
-
|
|
520
|
+
s.unsub?.();
|
|
524
521
|
s.dropdown.style.cssText = '';
|
|
525
522
|
s.dropdown.style.display = 'none';
|
|
526
523
|
|
|
@@ -1003,6 +1003,12 @@ class SelectMenu {
|
|
|
1003
1003
|
#CloseListener_mousedown(e) {
|
|
1004
1004
|
const eventTarget = dom.query.getEventTarget(e);
|
|
1005
1005
|
if (this.form.contains(eventTarget)) return;
|
|
1006
|
+
if (
|
|
1007
|
+
this.#$.ui.opendControllers?.some(
|
|
1008
|
+
({ form }) => form?.contains?.(eventTarget) && !form.contains(this.#refer),
|
|
1009
|
+
)
|
|
1010
|
+
)
|
|
1011
|
+
return;
|
|
1006
1012
|
if (!this.#refer.contains(eventTarget)) {
|
|
1007
1013
|
this.close();
|
|
1008
1014
|
} else if (!dom.check.isInputElement(eventTarget)) {
|
|
@@ -279,15 +279,12 @@ export class TableCellService {
|
|
|
279
279
|
* @description Sets the unmerge button visibility.
|
|
280
280
|
*/
|
|
281
281
|
setUnMergeButton() {
|
|
282
|
-
|
|
282
|
+
const hasMergedCells =
|
|
283
283
|
this.findMergedCells(
|
|
284
284
|
!this.#state.selectedCells?.length ? [this.#state.fixedCell] : this.#state.selectedCells,
|
|
285
|
-
).length > 0
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
} else {
|
|
289
|
-
this.unmergeButton.disabled = true;
|
|
290
|
-
}
|
|
285
|
+
).length > 0;
|
|
286
|
+
|
|
287
|
+
this.unmergeButton.style.display = hasMergedCells ? 'block' : 'none';
|
|
291
288
|
}
|
|
292
289
|
|
|
293
290
|
/**
|
|
@@ -44,6 +44,7 @@ export namespace A {
|
|
|
44
44
|
function deleteEmptyLineMergeNext(formatEl: Element, next: Element): Action;
|
|
45
45
|
function deleteBrLineRowMerge(rowEndBr: Node): Action;
|
|
46
46
|
function deleteSoftBreakMerge(br: Node): Action;
|
|
47
|
+
function mergeLineInto(into: HTMLElement, from: HTMLElement): Action;
|
|
47
48
|
function tabFormatIndent(range: Range, formatEl: Element, shift: boolean): Action;
|
|
48
49
|
function caretScrollTo(range: Range): Action;
|
|
49
50
|
function enterLineAddDefault(formatEl: Element): Action;
|
|
@@ -37,6 +37,10 @@ declare const _default: {
|
|
|
37
37
|
'delete.list.removeNested': ({ ports, ctx }: EffectContext_keydown, { range, formatEl, rangeEl }: any) => void;
|
|
38
38
|
/** @action deleteEmptyLineMergeNext — remove an empty line, move caret to the start of the next line */
|
|
39
39
|
'delete.emptyLine.mergeNext': ({ ports }: EffectContext_keydown, { formatEl, next }: any) => void;
|
|
40
|
+
/**
|
|
41
|
+
* @action mergeLineInto — Merge one line into another across a block boundary.
|
|
42
|
+
*/
|
|
43
|
+
'line.merge': ({ ports }: EffectContext_keydown, { into, from }: any) => void;
|
|
40
44
|
/** @action deleteBrLineRowMerge — remove an empty row inside a brLine (PRE), pull the next row up */
|
|
41
45
|
'delete.brline.rowMerge': ({ ports }: EffectContext_keydown, { rowEndBr }: any) => void;
|
|
42
46
|
/** @action deleteSoftBreakMerge */
|
|
@@ -29,6 +29,7 @@ import type {} from '../../typedef';
|
|
|
29
29
|
* @property {(...args: Parameters<Format['isNormalLine']>) => ReturnType<Format['isNormalLine']>} isNormalLine
|
|
30
30
|
* @property {(...args: Parameters<Format['isBrLine']>) => ReturnType<Format['isBrLine']>} isBrLine
|
|
31
31
|
* @property {(...args: Parameters<Format['isClosureBrLine']>) => ReturnType<Format['isClosureBrLine']>} isClosureBrLine
|
|
32
|
+
* @property {(...args: Parameters<Format['isBlock']>) => ReturnType<Format['isBlock']>} isBlock
|
|
32
33
|
* @property {(...args: Parameters<Format['isClosureBlock']>) => ReturnType<Format['isClosureBlock']>} isClosureBlock
|
|
33
34
|
* @property {(...args: Parameters<Format['isEdgeLine']>) => ReturnType<Format['isEdgeLine']>} isEdgeLine
|
|
34
35
|
* @property {(...args: Parameters<Format['removeBlock']>) => ReturnType<Format['removeBlock']>} removeBlock
|
|
@@ -98,6 +99,7 @@ export function makePorts(
|
|
|
98
99
|
isNormalLine: (n: any) => n is HTMLElement;
|
|
99
100
|
isBrLine: (n: any) => n is HTMLElement;
|
|
100
101
|
isClosureBrLine: (n: any) => n is HTMLElement;
|
|
102
|
+
isBlock: (n: any) => n is HTMLElement;
|
|
101
103
|
isClosureBlock: (n: any) => n is HTMLElement;
|
|
102
104
|
isEdgeLine: (node: any, offset: any, dir: any) => node is HTMLElement;
|
|
103
105
|
removeBlock: (
|
|
@@ -206,6 +208,7 @@ export type FormatPorts = {
|
|
|
206
208
|
isNormalLine: (...args: Parameters<Format['isNormalLine']>) => ReturnType<Format['isNormalLine']>;
|
|
207
209
|
isBrLine: (...args: Parameters<Format['isBrLine']>) => ReturnType<Format['isBrLine']>;
|
|
208
210
|
isClosureBrLine: (...args: Parameters<Format['isClosureBrLine']>) => ReturnType<Format['isClosureBrLine']>;
|
|
211
|
+
isBlock: (...args: Parameters<Format['isBlock']>) => ReturnType<Format['isBlock']>;
|
|
209
212
|
isClosureBlock: (...args: Parameters<Format['isClosureBlock']>) => ReturnType<Format['isClosureBlock']>;
|
|
210
213
|
isEdgeLine: (...args: Parameters<Format['isEdgeLine']>) => ReturnType<Format['isEdgeLine']>;
|
|
211
214
|
removeBlock: (...args: Parameters<Format['removeBlock']>) => ReturnType<Format['removeBlock']>;
|