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.
Files changed (51) hide show
  1. package/README.md +18 -1
  2. package/dist/suneditor-contents.min.css +1 -1
  3. package/dist/suneditor.min.css +3 -3
  4. package/dist/suneditor.min.js +1 -1
  5. package/package.json +4 -4
  6. package/src/assets/design/color.css +5 -2
  7. package/src/assets/design/size.css +6 -2
  8. package/src/assets/icons/defaultIcons.js +195 -162
  9. package/src/assets/suneditor.css +5101 -5105
  10. package/src/core/config/eventManager.js +24 -1
  11. package/src/core/config/optionProvider.js +5 -4
  12. package/src/core/event/actions/index.js +8 -4
  13. package/src/core/event/effects/keydown.registry.js +59 -10
  14. package/src/core/event/ports.js +2 -0
  15. package/src/core/event/rules/keydown.rule.backspace.js +52 -1
  16. package/src/core/event/rules/keydown.rule.delete.js +51 -0
  17. package/src/core/logic/dom/format.js +1 -1
  18. package/src/core/logic/dom/html.js +31 -0
  19. package/src/core/logic/panel/blockHandle.js +61 -24
  20. package/src/core/logic/panel/blockResolver.js +41 -4
  21. package/src/core/logic/panel/finder.js +3 -0
  22. package/src/core/logic/panel/menu.js +38 -1
  23. package/src/core/logic/panel/toolbar.js +1 -0
  24. package/src/core/logic/shell/_commandExecutor.js +2 -1
  25. package/src/core/logic/shell/pluginManager.js +15 -2
  26. package/src/core/logic/shell/ui.js +73 -32
  27. package/src/core/schema/frameContext.js +1 -1
  28. package/src/core/schema/options.js +49 -3
  29. package/src/core/section/constructor.js +17 -9
  30. package/src/modules/contract/Controller.js +14 -0
  31. package/src/modules/contract/Figure.js +5 -2
  32. package/src/modules/ui/CommandMenu.js +53 -14
  33. package/src/modules/ui/SelectMenu.js +103 -36
  34. package/src/plugins/dropdown/table/index.js +25 -8
  35. package/src/plugins/dropdown/table/services/table.cell.js +4 -7
  36. package/src/plugins/dropdown/table/shared/table.constants.js +2 -0
  37. package/src/plugins/field/slashCommand.js +59 -12
  38. package/types/assets/icons/defaultIcons.d.ts +1 -0
  39. package/types/core/event/actions/index.d.ts +1 -0
  40. package/types/core/event/effects/keydown.registry.d.ts +4 -0
  41. package/types/core/event/ports.d.ts +3 -0
  42. package/types/core/logic/panel/blockHandle.d.ts +3 -16
  43. package/types/core/logic/panel/menu.d.ts +8 -0
  44. package/types/core/logic/shell/ui.d.ts +31 -5
  45. package/types/core/schema/frameContext.d.ts +6 -2
  46. package/types/core/schema/options.d.ts +95 -4
  47. package/types/modules/ui/CommandMenu.d.ts +11 -0
  48. package/types/modules/ui/SelectMenu.d.ts +13 -7
  49. package/types/plugins/dropdown/table/index.d.ts +11 -0
  50. package/types/plugins/dropdown/table/shared/table.constants.d.ts +1 -0
  51. 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
  */
@@ -95,6 +95,7 @@ class Toolbar {
95
95
  isStickyPosible &&
96
96
  stickyTop >= 0 &&
97
97
  !this.#options.get('toolbar_container') &&
98
+ !this.#options.get('_toolbar_sticky_fixed') &&
98
99
  typeof CSS !== 'undefined' &&
99
100
  CSS.supports('position', 'sticky');
100
101
 
@@ -93,7 +93,8 @@ export default class CommandExecutor {
93
93
  }
94
94
  break;
95
95
  case 'finder':
96
- this.#$.finder.open(true);
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
- const infoLst = domParser.querySelectorAll(query);
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 === true || retainFilter[plugin.key] !== false) plugin.method(infoLst[i]);
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.menu,
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 on it.
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 text = fc.get('placeholder_line');
885
- if (!text) return false;
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 inTableCell = !!line && !!dom.query.getParentElement(line, dom.check.isTableCell);
891
- const target = dom.check.isEmptyLine(line) && !dom.check.isListCell(line) && !inTableCell ? line : null;
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: number}} [toolbar_sticky=0] - Enables sticky toolbar.
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 pluginsValues = (Array.isArray(originPlugins) ? originPlugins : Object.keys(originPlugins))
48
- .filter((name) => !excludedPlugins.includes(name))
49
- .map((name) => originPlugins[name]);
50
-
51
- for (let i = 0, len = pluginsValues.length, p; i < len; i++) {
52
- p = pluginsValues[i].default || pluginsValues[i];
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 = 0;
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 = o.get('_rtl') ? subbar.buttonList.reverse() : subbar.buttonList;
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: o.get('_rtl') ? buttonList.reverse() : buttonList,
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 || originEl;
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;