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
|
@@ -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'));
|
|
@@ -151,9 +151,22 @@ class PluginManager {
|
|
|
151
151
|
let retainFilter;
|
|
152
152
|
if ((retainFilter = this.#options.get('__pluginRetainFilter'))) {
|
|
153
153
|
this.#retainFormatCheckers.forEach((plugin, query) => {
|
|
154
|
-
|
|
154
|
+
let infoLst;
|
|
155
|
+
try {
|
|
156
|
+
infoLst = domParser.querySelectorAll(query);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn(`[SUNEDITOR.retainFormat.fail]-[${plugin.key}]`, error.message);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
155
162
|
for (let i = 0, len = infoLst.length; i < len; i++) {
|
|
156
|
-
if (retainFilter
|
|
163
|
+
if (retainFilter !== true && retainFilter[plugin.key] === false) continue;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
plugin.method(infoLst[i]);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn(`[SUNEDITOR.retainFormat.fail]-[${plugin.key}]`, error.message);
|
|
169
|
+
}
|
|
157
170
|
}
|
|
158
171
|
});
|
|
159
172
|
}
|
|
@@ -72,6 +72,16 @@ class UIManager {
|
|
|
72
72
|
*/
|
|
73
73
|
#blockHandle = null;
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @description Currently open `SelectMenu` instances.
|
|
77
|
+
* - There is one editor-wide "a select menu is open" flag but many `SelectMenu` instances, so it
|
|
78
|
+
* cannot be a plain boolean: the last writer would win. A field plugin that closes its own menu on
|
|
79
|
+
* every keystroke (`autocomplete`) used to flip the flag off while another menu (`slashCommand`) was
|
|
80
|
+
* still open, which let the wysiwyg keydown handler run against the open menu.
|
|
81
|
+
* @type {Set<*>}
|
|
82
|
+
*/
|
|
83
|
+
#openSelectMenus = new Set();
|
|
84
|
+
|
|
75
85
|
/**
|
|
76
86
|
* @constructor
|
|
77
87
|
* @param {SunEditor.Kernel} kernel
|
|
@@ -110,12 +120,6 @@ class UIManager {
|
|
|
110
120
|
this.#closeSignal = false;
|
|
111
121
|
this.#backWrapper = /** @type {HTMLElement} */ (this.#carrierWrapper.querySelector('.se-back-wrapper'));
|
|
112
122
|
|
|
113
|
-
/**
|
|
114
|
-
* @description Whether `SelectMenu` is open
|
|
115
|
-
* @type {boolean}
|
|
116
|
-
*/
|
|
117
|
-
this.selectMenuOn = false;
|
|
118
|
-
|
|
119
123
|
/**
|
|
120
124
|
* @description Currently open `Controller` info array
|
|
121
125
|
* @type {Array<SunEditor.Module.Controller.Info>}
|
|
@@ -136,6 +140,27 @@ class UIManager {
|
|
|
136
140
|
this._figureContainer = null;
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/**
|
|
144
|
+
* @description Whether any `SelectMenu` is currently open.
|
|
145
|
+
* - Read-only: a menu announces itself through {@link setSelectMenuOpen}. Derived from the set of
|
|
146
|
+
* open instances so an unrelated menu closing cannot clear the flag for a menu that is still open.
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
get selectMenuOn() {
|
|
150
|
+
return this.#openSelectMenus.size > 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @internal
|
|
155
|
+
* @description `SelectMenu` open-state notification. Called by `SelectMenu.open()` / `.close()`.
|
|
156
|
+
* @param {*} instance The `SelectMenu` instance changing state
|
|
157
|
+
* @param {boolean} open `true` on open, `false` on close
|
|
158
|
+
*/
|
|
159
|
+
setSelectMenuOpen(instance, open) {
|
|
160
|
+
if (open) this.#openSelectMenus.add(instance);
|
|
161
|
+
else this.#openSelectMenus.delete(instance);
|
|
162
|
+
}
|
|
163
|
+
|
|
139
164
|
/**
|
|
140
165
|
* @description Set editor frame styles.
|
|
141
166
|
* - Define the style of the edit area
|
|
@@ -291,12 +316,6 @@ class UIManager {
|
|
|
291
316
|
|
|
292
317
|
this.#activeDirBtn(rtl);
|
|
293
318
|
|
|
294
|
-
// reverse toolbar buttons
|
|
295
|
-
this.#reverseToolbarButtons(this.#context.get('toolbar_buttonTray'));
|
|
296
|
-
if (this.#context.has('toolbar_sub_buttonTray')) {
|
|
297
|
-
this.#reverseToolbarButtons(this.#context.get('toolbar_sub_buttonTray'));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
319
|
if (this.#store.mode.isBalloon) this.#$.toolbar._showBalloon();
|
|
301
320
|
else if (this.#store.mode.isSubBalloon) this.#$.subToolbar._showBalloon();
|
|
302
321
|
} catch (e) {
|
|
@@ -614,7 +633,7 @@ class UIManager {
|
|
|
614
633
|
rt.get('blockHandle'),
|
|
615
634
|
rt.get('blockHandlePlus'),
|
|
616
635
|
rt.get('blockHandleDrag'),
|
|
617
|
-
blockHandleOpt
|
|
636
|
+
blockHandleOpt,
|
|
618
637
|
);
|
|
619
638
|
}
|
|
620
639
|
}
|
|
@@ -783,20 +802,6 @@ class UIManager {
|
|
|
783
802
|
}
|
|
784
803
|
}
|
|
785
804
|
|
|
786
|
-
/**
|
|
787
|
-
* @description Reverse the order of toolbar button groups (excluding the more-layer).
|
|
788
|
-
* @param {HTMLElement} buttonTray - The `.se-btn-tray` element.
|
|
789
|
-
*/
|
|
790
|
-
#reverseToolbarButtons(buttonTray) {
|
|
791
|
-
if (!buttonTray) return;
|
|
792
|
-
const moreLayer = buttonTray.querySelector('.se-toolbar-more-layer');
|
|
793
|
-
const children = Array.from(buttonTray.children).filter((c) => c !== moreLayer);
|
|
794
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
795
|
-
buttonTray.appendChild(children[i]);
|
|
796
|
-
}
|
|
797
|
-
if (moreLayer) buttonTray.appendChild(moreLayer);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
805
|
/**
|
|
801
806
|
* @internal
|
|
802
807
|
* @description Set the disabled button list
|
|
@@ -873,22 +878,58 @@ class UIManager {
|
|
|
873
878
|
}
|
|
874
879
|
}
|
|
875
880
|
|
|
881
|
+
/**
|
|
882
|
+
* @description Resolves the per-line placeholder text for an empty line from the `placeholder_line` option.
|
|
883
|
+
* - String option: one hint for every empty line — except list cells and table cells (backward compatible).
|
|
884
|
+
* - Object option: keyed by tag name or a category sentinel matching the editor's format classification
|
|
885
|
+
* ({@link Format#isNormalLine}, `isBrLine`, `isClosureBrLine`, `isBlock`, `isClosureBlock`, list cells).
|
|
886
|
+
* @param {?Node} line - The (empty) line element.
|
|
887
|
+
* @param {string|Object<string, string>} opt - The `placeholder_line` option value.
|
|
888
|
+
* @returns {string} The resolved placeholder text (`''` = none).
|
|
889
|
+
*/
|
|
890
|
+
resolveLinePlaceholder(line, opt) {
|
|
891
|
+
if (!line || !opt) return '';
|
|
892
|
+
|
|
893
|
+
if (typeof opt !== 'object') {
|
|
894
|
+
return dom.check.isListCell(line) || dom.query.getParentElement(line, dom.check.isTableCell) ? '' : opt;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const format = this.#$.format;
|
|
898
|
+
const tag = line.nodeName.toLowerCase();
|
|
899
|
+
|
|
900
|
+
if (typeof opt[tag] === 'string') return opt[tag];
|
|
901
|
+
if (dom.check.isListCell(line) && typeof opt['@list'] === 'string') return opt['@list'];
|
|
902
|
+
if (format.isClosureBrLine(line) && typeof opt['@closureBrLine'] === 'string') return opt['@closureBrLine'];
|
|
903
|
+
if (format.isBrLine(line) && typeof opt['@brLine'] === 'string') return opt['@brLine'];
|
|
904
|
+
|
|
905
|
+
const block = format.getBlock(line);
|
|
906
|
+
if (block && block !== line) {
|
|
907
|
+
const blockTag = block.nodeName.toLowerCase();
|
|
908
|
+
if (typeof opt[blockTag] === 'string') return opt[blockTag];
|
|
909
|
+
if (format.isClosureBlock(block) && typeof opt['@closureBlock'] === 'string') return opt['@closureBlock'];
|
|
910
|
+
if (typeof opt['@block'] === 'string') return opt['@block'];
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (format.isNormalLine(line) && typeof opt['@normalLine'] === 'string') return opt['@normalLine'];
|
|
914
|
+
return typeof opt['@line'] === 'string' ? opt['@line'] : '';
|
|
915
|
+
}
|
|
916
|
+
|
|
876
917
|
/**
|
|
877
918
|
* @internal
|
|
878
919
|
* @description Notion-style per-line placeholder. Marks the focused empty line so a CSS `::before`
|
|
879
|
-
* renders the hint text
|
|
920
|
+
* renders the hint text resolved for that line's type (see {@link resolveLinePlaceholder}).
|
|
880
921
|
* @param {SunEditor.FrameContext} fc - Frame context
|
|
881
922
|
* @returns {boolean} Whether the line placeholder is currently shown.
|
|
882
923
|
*/
|
|
883
924
|
#updateLinePlaceholder(fc) {
|
|
884
|
-
const
|
|
885
|
-
if (!
|
|
925
|
+
const opt = fc.get('placeholder_line');
|
|
926
|
+
if (!opt) return false;
|
|
886
927
|
|
|
887
928
|
const wysiwyg = fc.get('wysiwyg');
|
|
888
929
|
|
|
889
930
|
const line = this.#store.get('hasFocus') ? this.#$.format.getLine(this.#$.selection.selectionNode) : null;
|
|
890
|
-
const
|
|
891
|
-
const target =
|
|
931
|
+
const text = dom.check.isEmptyLine(line) ? this.resolveLinePlaceholder(line, opt) : '';
|
|
932
|
+
const target = text ? line : null;
|
|
892
933
|
|
|
893
934
|
const prevMarkers = wysiwyg.querySelectorAll('.se-placeholder-line');
|
|
894
935
|
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' } }
|
|
@@ -313,9 +327,25 @@ export const DEFAULTS = {
|
|
|
313
327
|
* menu: [
|
|
314
328
|
* 'p', 'heading', 'blockStyle',
|
|
315
329
|
* { title: 'Duplicate', icon: 'copy', action: ($, { block }) => block.after(block.cloneNode(true)) },
|
|
330
|
+
* // `'table'` as a string opens the size picker; a custom item inserts a default table directly
|
|
331
|
+
* { title: 'Table', icon: 'table', action: ($) => $.plugins.table.insert(3, 3) },
|
|
316
332
|
* ],
|
|
317
333
|
* }
|
|
318
334
|
* ```
|
|
335
|
+
* @property {string} [blockHandle.maxHeight=""] - Max height of the menu list. Any CSS length; the list scrolls past it.
|
|
336
|
+
* - Unset by default: the menu grows with its items and is only clamped when it would overflow the viewport.
|
|
337
|
+
* @property {string} [blockHandle.minWidth="200px"] - Min width of the menu.
|
|
338
|
+
* @property {function(SunEditor.Deps, { block: HTMLElement, openMenu: function(): void }): void} [blockHandle.onPlusClick] - Runs after the plus button inserted a new line.
|
|
339
|
+
* - Adding the line is fixed behavior; this hook decides what happens next. Nothing does by default.
|
|
340
|
+
* - `block` is the new line, already focused. `openMenu()` opens the block handle's own `menu`.
|
|
341
|
+
* ```js
|
|
342
|
+
* blockHandle: {
|
|
343
|
+
* // open the block handle menu
|
|
344
|
+
* onPlusClick: ($, { openMenu }) => openMenu(),
|
|
345
|
+
* // ...or the slash command menu
|
|
346
|
+
* onPlusClick: ($, { block }) => $.plugins.slashCommand.open(block),
|
|
347
|
+
* }
|
|
348
|
+
* ```
|
|
319
349
|
* @property {string} [type=""] - Editor type. Use `"document"` for a document-style layout, with optional sub-types after `:`.
|
|
320
350
|
* ```js
|
|
321
351
|
* // type
|
|
@@ -460,6 +490,13 @@ export const DEFAULTS = {
|
|
|
460
490
|
* - Formats that include `line`, such as "Quote", still operate on a `line` basis.
|
|
461
491
|
* - suneditor processes work in `line` units.
|
|
462
492
|
* - When set to `br`, performance may decrease when editing a lot of data.
|
|
493
|
+
* @property {boolean} [lineBreakClearStyle=false] - When `true`, pressing Enter at the **end** of a line
|
|
494
|
+
* starts a fresh line that does not carry the caret's inline style nodes (e.g. bold/italic/color spans, links);
|
|
495
|
+
* the line-level element and its attributes are preserved.
|
|
496
|
+
* Only affects the end-of-line case — mid-line splits, start-of-line breaks, and Shift+Enter are unchanged.
|
|
497
|
+
* ```js
|
|
498
|
+
* { lineBreakClearStyle: true }
|
|
499
|
+
* ```
|
|
463
500
|
* @property {string} [lineAttrReset=""] - Line properties that should be reset when changing lines. Delimiter: `"|"`.
|
|
464
501
|
* ```js
|
|
465
502
|
* { lineAttrReset: 'id|name' }
|
|
@@ -510,12 +547,16 @@ export const DEFAULTS = {
|
|
|
510
547
|
* { toolbar_innerWidth: 'auto' }
|
|
511
548
|
* ```
|
|
512
549
|
* @property {?HTMLElement} [toolbar_container] - Container element for the toolbar.
|
|
513
|
-
* @property {number|{top: number, offset
|
|
550
|
+
* @property {number|{top: number, offset?: number, position?: "sticky"|"fixed"}} [toolbar_sticky=0] - Enables sticky toolbar.
|
|
514
551
|
* - `number`: Sets the sticky top position (px). Use `-1` to disable sticky.
|
|
515
552
|
* - `{top, offset}`: `top` is the sticky position when the page header is visible.
|
|
516
553
|
* - `offset` is the sticky position when a virtual keyboard shifts the viewport (e.g., on tablets, touch devices).
|
|
517
554
|
* - When the virtual keyboard is active, `offset` replaces `top` so the toolbar doesn't leave a gap
|
|
518
555
|
* - for a page header that has scrolled out of view. Default `offset` is `0`.
|
|
556
|
+
* - `position` (default `"sticky"`): the positioning engine.
|
|
557
|
+
* - `"sticky"` uses native CSS `position: sticky` (with a JS `position: fixed` fallback where unsupported).
|
|
558
|
+
* `"fixed"` forces the JS `position: fixed` engine
|
|
559
|
+
* - even when CSS sticky is supported — for environments where CSS sticky silently misbehaves and can't be
|
|
519
560
|
* ```js
|
|
520
561
|
* // Basic usage — sticky at top with 0px offset
|
|
521
562
|
* toolbar_sticky: 0
|
|
@@ -525,6 +566,9 @@ export const DEFAULTS = {
|
|
|
525
566
|
*
|
|
526
567
|
* // 92px header on desktop, but 0px when virtual keyboard pushes the viewport
|
|
527
568
|
* toolbar_sticky: { top: 92, offset: 0 }
|
|
569
|
+
*
|
|
570
|
+
* // Force the JS position:fixed engine (CSS sticky unreliable in this environment)
|
|
571
|
+
* toolbar_sticky: { top: 0, position: 'fixed' }
|
|
528
572
|
* ```
|
|
529
573
|
* @property {boolean} [toolbar_hide=false] - Hides toolbar initially.
|
|
530
574
|
* @property {Object} [subToolbar={}] - Sub-toolbar configuration. A secondary toolbar that appears on text selection.
|
|
@@ -776,6 +820,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
776
820
|
printClass: true,
|
|
777
821
|
defaultLine: 'fixed',
|
|
778
822
|
defaultLineBreakFormat: true,
|
|
823
|
+
lineBreakClearStyle: true,
|
|
779
824
|
scopeSelectionTags: true,
|
|
780
825
|
__defaultElementWhitelist: 'fixed',
|
|
781
826
|
elementWhitelist: 'fixed',
|
|
@@ -822,7 +867,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
822
867
|
};
|
|
823
868
|
|
|
824
869
|
/**
|
|
825
|
-
* @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | '_toolbar_sticky' | '_toolbar_sticky_offset' | 'strictMode' | 'lineAttrReset'} TransformedOptionKeys
|
|
870
|
+
* @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | '_toolbar_sticky' | '_toolbar_sticky_offset' | '_toolbar_sticky_fixed' | 'strictMode' | 'lineAttrReset'} TransformedOptionKeys
|
|
826
871
|
*/
|
|
827
872
|
|
|
828
873
|
/**
|
|
@@ -846,6 +891,7 @@ export const OPTION_FIXED_FLAG = {
|
|
|
846
891
|
* @property {HTMLElement|null} toolbar_container
|
|
847
892
|
* @property {number} _toolbar_sticky
|
|
848
893
|
* @property {number} _toolbar_sticky_offset
|
|
894
|
+
* @property {boolean} _toolbar_sticky_fixed
|
|
849
895
|
* @property {StrictModeOptions} strictMode
|
|
850
896
|
* @property {string[]} lineAttrReset
|
|
851
897
|
*/
|
|
@@ -44,12 +44,16 @@ function Constructor(editorTargets, options) {
|
|
|
44
44
|
if (options.plugins) {
|
|
45
45
|
const excludedPlugins = options.excludedPlugins || [];
|
|
46
46
|
const originPlugins = options.plugins;
|
|
47
|
-
const
|
|
48
|
-
.
|
|
49
|
-
.map((name) => originPlugins[name]);
|
|
50
|
-
|
|
51
|
-
for (let i = 0, len =
|
|
52
|
-
|
|
47
|
+
const pluginsEntries = Array.isArray(originPlugins)
|
|
48
|
+
? originPlugins.map((plugin) => [null, plugin])
|
|
49
|
+
: Object.keys(originPlugins).map((name) => [name, originPlugins[name]]);
|
|
50
|
+
|
|
51
|
+
for (let i = 0, len = pluginsEntries.length, name, p; i < len; i++) {
|
|
52
|
+
name = pluginsEntries[i][0];
|
|
53
|
+
p = pluginsEntries[i][1];
|
|
54
|
+
p = p?.default || p;
|
|
55
|
+
if (!p?.key) continue;
|
|
56
|
+
if (excludedPlugins.includes(p.key) || (name !== null && excludedPlugins.includes(name))) continue;
|
|
53
57
|
plugins[p.key] = p;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -89,7 +93,7 @@ function Constructor(editorTargets, options) {
|
|
|
89
93
|
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
94
|
})
|
|
91
95
|
);
|
|
92
|
-
focusTemp.tabIndex =
|
|
96
|
+
focusTemp.tabIndex = -1;
|
|
93
97
|
editor_carrier_wrapper.appendChild(focusTemp);
|
|
94
98
|
|
|
95
99
|
// modal
|
|
@@ -749,6 +753,7 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
749
753
|
typeof options.defaultLine === 'string' && options.defaultLine.length > 0 ? options.defaultLine : 'p',
|
|
750
754
|
);
|
|
751
755
|
o.set('defaultLineBreakFormat', options.defaultLineBreakFormat || 'line');
|
|
756
|
+
o.set('lineBreakClearStyle', !!options.lineBreakClearStyle);
|
|
752
757
|
o.set('scopeSelectionTags', options.scopeSelectionTags || DEFAULTS.SCOPE_SELECTION_TAGS);
|
|
753
758
|
// element
|
|
754
759
|
const elw = (typeof options.elementWhitelist === 'string' ? options.elementWhitelist : '').toLowerCase();
|
|
@@ -893,12 +898,15 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
893
898
|
if (_isBalloon) {
|
|
894
899
|
o.set('_toolbar_sticky', -1);
|
|
895
900
|
o.set('_toolbar_sticky_offset', 0);
|
|
901
|
+
o.set('_toolbar_sticky_fixed', false);
|
|
896
902
|
} else if (_stickyOpt !== null && typeof _stickyOpt === 'object') {
|
|
897
903
|
o.set('_toolbar_sticky', numbers.get(_stickyOpt.top, 0));
|
|
898
904
|
o.set('_toolbar_sticky_offset', numbers.get(_stickyOpt.offset, 0));
|
|
905
|
+
o.set('_toolbar_sticky_fixed', _stickyOpt.position === 'fixed');
|
|
899
906
|
} else {
|
|
900
907
|
o.set('_toolbar_sticky', _stickyOpt === undefined ? 0 : numbers.is(_stickyOpt) ? _stickyOpt : -1);
|
|
901
908
|
o.set('_toolbar_sticky_offset', 0);
|
|
909
|
+
o.set('_toolbar_sticky_fixed', false);
|
|
902
910
|
}
|
|
903
911
|
|
|
904
912
|
o.set('toolbar_hide', !!options.toolbar_hide);
|
|
@@ -917,7 +925,7 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
917
925
|
'toolbar_sub_width',
|
|
918
926
|
subbar.width ? (numbers.is(subbar.width) ? subbar.width + 'px' : subbar.width) : 'auto',
|
|
919
927
|
);
|
|
920
|
-
subButtons =
|
|
928
|
+
subButtons = subbar.buttonList;
|
|
921
929
|
o.set('buttons_sub', new Set(subButtons.toString().split(',')));
|
|
922
930
|
}
|
|
923
931
|
}
|
|
@@ -1050,7 +1058,7 @@ export function InitOptions(options, editorTargets, plugins) {
|
|
|
1050
1058
|
i: icons,
|
|
1051
1059
|
l: /** @type {Object<string, string>} */ (options.lang || _defaultLang),
|
|
1052
1060
|
v: (options.value = typeof options.value === 'string' ? options.value : null),
|
|
1053
|
-
buttons:
|
|
1061
|
+
buttons: buttonList,
|
|
1054
1062
|
subButtons: subButtons,
|
|
1055
1063
|
statusbarContainer:
|
|
1056
1064
|
typeof options.statusbar_container === 'string'
|
|
@@ -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
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Controller from './Controller';
|
|
2
2
|
import SelectMenu from '../ui/SelectMenu';
|
|
3
3
|
import { _DragHandle } from '../ui/_DragHandle';
|
|
4
|
-
import { dom, numbers, env, converter, keyCodeMap } from '../../helper';
|
|
4
|
+
import { dom, numbers, env, converter, keyCodeMap, unicode } from '../../helper';
|
|
5
5
|
|
|
6
6
|
const { _w, ON_OVER_COMPONENT } = env;
|
|
7
7
|
const DIRECTION_CURSOR_MAP = {
|
|
@@ -975,10 +975,13 @@ class Figure {
|
|
|
975
975
|
retainFigureFormat(container, originEl, anchorCover, fileManagerInst) {
|
|
976
976
|
const isInline = this.#$.component.isInline(container);
|
|
977
977
|
const originParent = originEl.parentNode;
|
|
978
|
+
const isBareWrapper =
|
|
979
|
+
originParent.children?.length === 1 &&
|
|
980
|
+
!originParent.textContent.replace(unicode.zeroWidthRegExp, '').trim();
|
|
978
981
|
let existElement =
|
|
979
982
|
this.#$.format.isBlock(originParent) || dom.check.isWysiwygFrame(originParent) || originParent.nodeType >= 9
|
|
980
983
|
? originEl
|
|
981
|
-
: Figure.GetContainer(originEl)?.container || originParent
|
|
984
|
+
: Figure.GetContainer(originEl)?.container || (isBareWrapper ? originParent : originEl);
|
|
982
985
|
|
|
983
986
|
if (dom.query.getParentElement(originEl, dom.check.isExcludeFormat)) {
|
|
984
987
|
existElement = anchorCover && anchorCover !== originEl ? anchorCover : originEl;
|