suneditor 3.0.1 → 3.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "suneditor",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "Vanilla JavaScript based WYSIWYG web editor",
5
5
  "author": "Yi JiHong",
6
6
  "license": "MIT",
@@ -1,24 +1,24 @@
1
1
  .sun-editor,
2
2
  .sun-editor-editable {
3
3
  /** --------------------------- content - [typography] ----------- */
4
- --se-edit-font-size: 13px;
4
+ --se-edit-font-size: 16px;
5
5
  --se-edit-line-height: 1.5em;
6
6
 
7
7
  /** --------------------------- layout - [typography] ----------- */
8
8
  /* main */
9
9
  --se-main-font-family: Helvetica Neue;
10
10
  --se-content-font-family: Helvetica Neue;
11
- --se-main-font-size: 13px;
11
+ --se-main-font-size: 14px;
12
12
 
13
13
  /* codeview, markdown font */
14
14
  --se-markdown-font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
15
15
  --se-codeview-font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
16
16
 
17
17
  /* button */
18
- --se-btn-font-size: 12px;
18
+ --se-btn-font-size: 14px;
19
19
 
20
20
  /* status */
21
- --se-statusbar-font-size: 10px;
21
+ --se-statusbar-font-size: 12px;
22
22
 
23
23
  /* modal */
24
24
  --se-modal-title-font-size: 15px;
@@ -916,13 +916,13 @@
916
916
  .sun-editor .se-toolbar.se-toolbar-bottom {
917
917
  top: auto;
918
918
  bottom: 0;
919
- outline-offset: -1px;
920
919
  }
921
920
 
922
921
  /* Bottom toolbar: more layer opens above buttons */
923
922
  .sun-editor .se-toolbar.se-toolbar-bottom .se-btn-tray {
924
923
  display: flex;
925
924
  flex-wrap: wrap;
925
+ padding: 4px 3px;
926
926
  }
927
927
 
928
928
  .sun-editor .se-toolbar.se-toolbar-bottom .se-btn-tray > .se-toolbar-more-layer {
@@ -934,6 +934,7 @@
934
934
  .sun-editor .se-toolbar.se-toolbar-bottom .se-toolbar-more-layer .se-more-layer {
935
935
  border-top: none;
936
936
  border-bottom: 1px solid var(--se-main-divider-color);
937
+ margin-bottom: 4px;
937
938
  }
938
939
 
939
940
  /* JS fallback sticky for bottom */
@@ -1836,7 +1837,7 @@
1836
1837
  display: flex;
1837
1838
  width: auto;
1838
1839
  height: auto;
1839
- min-height: 18px;
1840
+ min-height: 20px;
1840
1841
  border-top: 1px solid var(--se-main-divider-color);
1841
1842
  padding: 0 4px;
1842
1843
  background-color: var(--se-main-background-color);
@@ -3768,7 +3769,7 @@
3768
3769
  border-radius: var(--se-border-radius);
3769
3770
  background-color: var(--se-main-background-color);
3770
3771
  color: var(--se-main-color);
3771
- font-size: 13px;
3772
+ font-size: var(--se-main-font-size);
3772
3773
  outline: none;
3773
3774
  box-sizing: border-box;
3774
3775
  }
@@ -138,6 +138,21 @@ class EventOrchestrator extends KernelInjector {
138
138
  }, 250);
139
139
  }
140
140
 
141
+ /**
142
+ * @description Toggle toolbar-balloon with delay (debounced for selectionchange).
143
+ */
144
+ #toggleToolbarBalloonDelay() {
145
+ if (this.#balloonDelay) {
146
+ _w.clearTimeout(this.#balloonDelay);
147
+ }
148
+
149
+ this.#balloonDelay = _w.setTimeout(() => {
150
+ _w.clearTimeout(this.#balloonDelay);
151
+ this.#balloonDelay = null;
152
+ this._toggleToolbarBalloon();
153
+ }, 250);
154
+ }
155
+
141
156
  /**
142
157
  * @internal
143
158
  * @description Show or hide the toolbar-balloon.
@@ -909,17 +924,23 @@ class EventOrchestrator extends KernelInjector {
909
924
  }
910
925
 
911
926
  #OnResize_viewport() {
912
- if (isMobile && this.#options.get('toolbar_sticky') > -1) {
927
+ if (isMobile && this.#options.get('_toolbar_sticky') > -1) {
913
928
  this.#toolbar._resetSticky();
914
929
  this.#menu.__restoreMenuPosition();
915
930
  }
916
931
 
917
932
  this.#scrollContainer();
933
+
934
+ const prevHeight = this.#store.get('currentViewportHeight');
918
935
  this.__setViewportSize();
936
+
937
+ if (isMobile && prevHeight > 0 && prevHeight - _w.visualViewport.height > 100 && this.#store.get('hasFocus')) {
938
+ this.$.selection.scrollTo(this.$.selection.getRange(), { behavior: 'auto', block: 'nearest', inline: 'nearest' });
939
+ }
919
940
  }
920
941
 
921
942
  #OnScroll_window() {
922
- if (this.#options.get('toolbar_sticky') > -1) {
943
+ if (this.#options.get('_toolbar_sticky') > -1) {
923
944
  this.#toolbar._resetSticky();
924
945
  }
925
946
 
@@ -938,7 +959,7 @@ class EventOrchestrator extends KernelInjector {
938
959
  }
939
960
 
940
961
  #OnMobileScroll_viewport() {
941
- if (this.#options.get('toolbar_sticky') > -1) {
962
+ if (this.#options.get('_toolbar_sticky') > -1) {
942
963
  this.#toolbar._resetSticky();
943
964
  this.#menu.__restoreMenuPosition();
944
965
  }
@@ -959,6 +980,11 @@ class EventOrchestrator extends KernelInjector {
959
980
  this.$.selection.init();
960
981
  this.applyTagEffect();
961
982
 
983
+ // balloon toolbar - touch devices
984
+ if (isTouchDevice && (this.#store.mode.isBalloon || this.#store.mode.isSubBalloon)) {
985
+ this.#toggleToolbarBalloonDelay();
986
+ }
987
+
962
988
  // document type
963
989
  if (root.has('documentType_use_header')) {
964
990
  const el = dom.query.getParentElement(this.$.selection.selectionNode, this.$.format.isLine.bind(this.$.format));
@@ -158,14 +158,14 @@ export function makePorts(inst, { _styleNodes }) {
158
158
 
159
159
  // === enter event specific ===
160
160
  /**
161
- * @description Scrolls the editor view to the caret position after pressing `Enter`. (Ignored on mobile devices)
161
+ * @description Scrolls the editor view to the caret position after pressing `Enter`.
162
162
  * @param {Range} range Range object
163
163
  */
164
164
  enterScrollTo(range) {
165
165
  ui._iframeAutoHeight(frameContext);
166
166
 
167
167
  // scroll to
168
- if (isMobile && inst.scrollparents.length > 0) return;
168
+ // if (isMobile && inst.scrollparents.length > 0) return;
169
169
  selection.scrollTo(range, { behavior: 'auto', block: 'nearest', inline: 'nearest' });
170
170
  },
171
171
  /**
@@ -2134,6 +2134,14 @@ function _isSafeURL(url) {
2134
2134
  return _SAFE_URL_PROTOCOL.test(normalized) || !_RE_COLON.test(normalized);
2135
2135
  }
2136
2136
 
2137
+ /**
2138
+ * @description Checks whether an HTML attribute string is safe from XSS injection.
2139
+ * - Non-URL attributes (anything other than `href` or `src`) are always safe.
2140
+ * - For URL attributes, extracts the URL value and delegates to {@link _isSafeURL}
2141
+ * to verify the protocol against the allowed whitelist.
2142
+ * @param {string} attr A single attribute string (e.g. `href="https://example.com"`, `class="foo"`)
2143
+ * @returns {boolean} `true` if the attribute is safe, `false` if it contains a dangerous URL protocol.
2144
+ */
2137
2145
  function _isSafeAttribute(attr) {
2138
2146
  if (!_URL_ATTR_PATTERN.test(attr)) return true;
2139
2147
 
@@ -503,7 +503,7 @@ class Offset {
503
503
  const th = this.#context.get('toolbar_main').offsetHeight;
504
504
  const containerToolbar = this.#options.get('toolbar_container');
505
505
  const headLess = this.#store.mode.isBalloon || this.#store.mode.isInline || containerToolbar;
506
- const toolbarH = (containerToolbar && globalTop - wScrollY - th > 0) || (!this.#$.toolbar.isSticky && headLess) ? 0 : th + (this.#$.toolbar.isSticky ? this.#options.get('toolbar_sticky') : 0);
506
+ const toolbarH = (containerToolbar && globalTop - wScrollY - th > 0) || (!this.#$.toolbar.isSticky && headLess) ? 0 : th + (this.#$.toolbar.isSticky ? this.#options.get('_toolbar_sticky') : 0);
507
507
  const statusBarH = this.#frameContext.get('statusbar')?.offsetHeight || 0;
508
508
 
509
509
  // check margin
@@ -425,7 +425,7 @@ class Selection_ {
425
425
  const isBottom = this.#store.mode.isBottom;
426
426
  const realToolbarHeight = this.#context.get('toolbar_main').offsetHeight;
427
427
  const toolbarHeight = this.#$.toolbar.isSticky ? realToolbarHeight : 0;
428
- const positionToolbarHeight = this.#$.toolbar.isSticky ? toolbarHeight + this.#options.get('toolbar_sticky') : toolbarHeight;
428
+ const positionToolbarHeight = this.#$.toolbar.isSticky ? toolbarHeight + this.#options.get('_toolbar_sticky') : toolbarHeight;
429
429
  const statusbarHeight = this.#frameContext.get('statusbar')?.offsetHeight || 0;
430
430
 
431
431
  if (this.#hasScrollParents) {
@@ -154,7 +154,7 @@ class Finder {
154
154
  */
155
155
  #updateStickyTop() {
156
156
  if (!this.#isOpen || !this.#panel) return;
157
- const stickyTop = this.#$.options.get('toolbar_sticky');
157
+ const stickyTop = this.#$.options.get('_toolbar_sticky');
158
158
  if (this.#store.mode.isBottom) {
159
159
  this.#panel.style.top = 'auto';
160
160
  this.#panel.style.bottom = stickyTop >= 0 ? stickyTop + this.#$.context.get('toolbar_main').offsetHeight + 'px' : '0px';
@@ -28,6 +28,7 @@ class Toolbar {
28
28
 
29
29
  #useCSSSticky = false;
30
30
  #_isStickyFlag = false;
31
+ #_cssStickyShifted = false;
31
32
 
32
33
  /**
33
34
  * @constructor
@@ -88,7 +89,7 @@ class Toolbar {
88
89
 
89
90
  // CSS sticky: non-balloon, non-inline, non-container, sticky enabled
90
91
  const isStickyPosible = !this.isSub && !balloon && !inline;
91
- const stickyTop = this.#options.get('toolbar_sticky');
92
+ const stickyTop = this.#options.get('_toolbar_sticky');
92
93
 
93
94
  this.#useCSSSticky = isStickyPosible && stickyTop >= 0 && !this.#options.get('toolbar_container') && typeof CSS !== 'undefined' && CSS.supports('position', 'sticky');
94
95
 
@@ -119,14 +120,15 @@ class Toolbar {
119
120
  */
120
121
  get isSticky() {
121
122
  if (this.isSub) return false;
122
- const stickyTop = this.#options.get('toolbar_sticky');
123
+ const stickyTop = this.#options.get('_toolbar_sticky');
123
124
  if (stickyTop < 0) return false;
124
125
 
125
126
  if (this.#useCSSSticky) {
126
127
  const toolbar = this.#context.get(this.keyName.main);
127
128
  if (!toolbar || toolbar.offsetWidth === 0 || toolbar.style.display === 'none') return false;
128
129
  if (this.isBottomMode) {
129
- return toolbar.getBoundingClientRect().bottom >= _w.innerHeight - stickyTop - 1;
130
+ const viewportHeight = this.#isViewPortSize ? _w.visualViewport.height : _w.innerHeight;
131
+ return toolbar.getBoundingClientRect().bottom >= viewportHeight - stickyTop - 1;
130
132
  }
131
133
  return toolbar.getBoundingClientRect().top <= stickyTop + 1;
132
134
  }
@@ -254,13 +256,16 @@ class Toolbar {
254
256
  * @description Reset the sticky toolbar position based on the editor state.
255
257
  */
256
258
  _resetSticky() {
257
- if (this.#useCSSSticky) return;
259
+ if (this.#useCSSSticky) {
260
+ this.#resetCSSStickyOffset();
261
+ return;
262
+ }
258
263
 
259
264
  const wrapper = this.#frameContext.get('wrapper');
260
265
  if (!wrapper) return;
261
266
 
262
267
  const toolbar = this.#context.get(this.keyName.main);
263
- const stickyTop = this.#options.get('toolbar_sticky');
268
+ const stickyTop = this.#options.get('_toolbar_sticky');
264
269
  if (this.#frameContext.get('isFullScreen') || toolbar.offsetWidth === 0 || stickyTop < 0) return;
265
270
 
266
271
  const currentScrollY = this.#isViewPortSize ? _w.visualViewport.pageTop : _w.scrollY;
@@ -485,11 +490,11 @@ class Toolbar {
485
490
  }
486
491
 
487
492
  if (this.isBottomMode) {
488
- const toolbarBottomPosition = this.#options.get('toolbar_sticky') + this.#getViewportTop();
493
+ const toolbarBottomPosition = this.#options.get('_toolbar_sticky') + this.#getViewportTop();
489
494
  toolbar.style.bottom = `${toolbarBottomPosition}px`;
490
495
  toolbar.style.top = 'auto';
491
496
  } else {
492
- const toolbarTopPosition = this.#options.get('toolbar_sticky') + inlineOffset + this.#getViewportTop();
497
+ const toolbarTopPosition = this.#options.get('_toolbar_sticky') + inlineOffset + this.#getViewportTop();
493
498
  toolbar.style.top = `${toolbarTopPosition}px`;
494
499
  }
495
500
  toolbar.style.width = this.isInlineMode ? this.inlineToolbarAttr.width : toolbar.offsetWidth + 'px';
@@ -508,6 +513,43 @@ class Toolbar {
508
513
  return 0;
509
514
  }
510
515
 
516
+ /**
517
+ * @description Adjust CSS sticky toolbar position when the mobile virtual keyboard changes the visual viewport.
518
+ */
519
+ #resetCSSStickyOffset() {
520
+ if (!this.#isViewPortSize) return;
521
+ // When the editor is inside a scrollable container (e.g., modal),
522
+ // position:sticky is relative to that container, not the viewport.
523
+ if (this.#kernel._eventOrchestrator.scrollparents.length > 0) return;
524
+
525
+ const viewportOffset = Math.round(_w.visualViewport.offsetTop);
526
+ if (viewportOffset === 0 && !this.#_cssStickyShifted) return;
527
+
528
+ const toolbar = this.#context.get(this.keyName.main);
529
+ const stickyOffset = this.#options.get('_toolbar_sticky_offset');
530
+
531
+ if (viewportOffset > 0) {
532
+ this.#_cssStickyShifted = true;
533
+ if (this.isBottomMode) {
534
+ const viewportBottom = Math.round(_w.innerHeight - _w.visualViewport.height - viewportOffset);
535
+ toolbar.style.bottom = stickyOffset + viewportBottom + 'px';
536
+ toolbar.style.top = 'auto';
537
+ } else {
538
+ toolbar.style.top = stickyOffset + viewportOffset + 'px';
539
+ }
540
+ } else {
541
+ // restore original CSS sticky value
542
+ this.#_cssStickyShifted = false;
543
+ const stickyTop = this.#options.get('_toolbar_sticky');
544
+ if (this.isBottomMode) {
545
+ toolbar.style.bottom = stickyTop + 'px';
546
+ toolbar.style.top = 'auto';
547
+ } else {
548
+ toolbar.style.top = stickyTop > 0 ? stickyTop + 'px' : '';
549
+ }
550
+ }
551
+ }
552
+
511
553
  /**
512
554
  * @description Disable `sticky` toolbar mode.
513
555
  */
@@ -411,7 +411,7 @@ class Viewer {
411
411
  this.#toolbarParent = null;
412
412
  }
413
413
 
414
- if (this.#options.get('toolbar_sticky') > -1) {
414
+ if (this.#options.get('_toolbar_sticky') > -1) {
415
415
  dom.utils.removeClass(toolbar, 'se-toolbar-sticky');
416
416
  }
417
417
 
@@ -460,7 +460,22 @@ export const DEFAULTS = {
460
460
  * @property {boolean} [tabDisable=false] - Disables tab key input.
461
461
  * @property {string} [toolbar_width="auto"] - Toolbar width.
462
462
  * @property {?HTMLElement} [toolbar_container] - Container element for the toolbar.
463
- * @property {number} [toolbar_sticky=0] - Enables sticky toolbar with optional offset.
463
+ * @property {number|{top: number, offset: number}} [toolbar_sticky=0] - Enables sticky toolbar.
464
+ * - `number`: Sets the sticky top position (px). Use `-1` to disable sticky.
465
+ * - `{top, offset}`: `top` is the sticky position when the page header is visible.
466
+ * - `offset` is the sticky position when a virtual keyboard shifts the viewport (e.g., on tablets, touch devices).
467
+ * - When the virtual keyboard is active, `offset` replaces `top` so the toolbar doesn't leave a gap
468
+ * - for a page header that has scrolled out of view. Default `offset` is `0`.
469
+ * ```js
470
+ * // Basic usage — sticky at top with 0px offset
471
+ * toolbar_sticky: 0
472
+ *
473
+ * // Account for a 92px fixed/sticky site header
474
+ * toolbar_sticky: 92
475
+ *
476
+ * // 92px header on desktop, but 0px when virtual keyboard pushes the viewport
477
+ * toolbar_sticky: { top: 92, offset: 0 }
478
+ * ```
464
479
  * @property {boolean} [toolbar_hide=false] - Hides toolbar initially.
465
480
  * @property {Object} [subToolbar={}] - Sub-toolbar configuration. A secondary toolbar that appears on text selection.
466
481
  * @property {SunEditor.UI.ButtonList} [subToolbar.buttonList] - List of Sub-toolbar buttons, grouped by sub-arrays.
@@ -754,7 +769,7 @@ export const OPTION_FIXED_FLAG = {
754
769
  };
755
770
 
756
771
  /**
757
- * @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | 'toolbar_sticky' | 'strictMode' | 'lineAttrReset'} TransformedOptionKeys
772
+ * @typedef {'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | '_toolbar_sticky' | '_toolbar_sticky_offset' | 'strictMode' | 'lineAttrReset'} TransformedOptionKeys
758
773
  */
759
774
 
760
775
  /**
@@ -776,7 +791,8 @@ export const OPTION_FIXED_FLAG = {
776
791
  * @property {{ reg: RegExp, str: string }} formatBlock
777
792
  * @property {string} toolbar_width
778
793
  * @property {HTMLElement|null} toolbar_container
779
- * @property {number} toolbar_sticky
794
+ * @property {number} _toolbar_sticky
795
+ * @property {number} _toolbar_sticky_offset
780
796
  * @property {StrictModeOptions} strictMode
781
797
  * @property {string[]} lineAttrReset
782
798
  */
@@ -616,7 +616,20 @@ export function InitOptions(options, editorTargets, plugins) {
616
616
  /** Toolbar */
617
617
  o.set('toolbar_width', options.toolbar_width ? (numbers.is(options.toolbar_width) ? options.toolbar_width + 'px' : options.toolbar_width) : 'auto');
618
618
  o.set('toolbar_container', options.toolbar_container && !/inline/i.test(o.get('mode')) ? (typeof options.toolbar_container === 'string' ? _d.querySelector(options.toolbar_container) : options.toolbar_container) : null);
619
- o.set('toolbar_sticky', /balloon/i.test(o.get('mode')) ? -1 : options.toolbar_sticky === undefined ? 0 : numbers.is(options.toolbar_sticky) ? numbers.get(options.toolbar_sticky, 0) : -1);
619
+
620
+ const _stickyOpt = options.toolbar_sticky;
621
+ const _isBalloon = /balloon/i.test(o.get('mode'));
622
+ if (_isBalloon) {
623
+ o.set('_toolbar_sticky', -1);
624
+ o.set('_toolbar_sticky_offset', 0);
625
+ } else if (_stickyOpt !== null && typeof _stickyOpt === 'object') {
626
+ o.set('_toolbar_sticky', numbers.get(_stickyOpt.top, 0));
627
+ o.set('_toolbar_sticky_offset', numbers.get(_stickyOpt.offset, 0));
628
+ } else {
629
+ o.set('_toolbar_sticky', _stickyOpt === undefined ? 0 : numbers.is(_stickyOpt) ? _stickyOpt : -1);
630
+ o.set('_toolbar_sticky_offset', 0);
631
+ }
632
+
620
633
  o.set('toolbar_hide', !!options.toolbar_hide);
621
634
 
622
635
  /** subToolbar */
@@ -180,6 +180,7 @@ class ApiManager {
180
180
  * @returns
181
181
  */
182
182
  #normalizeUrl(url) {
183
+ if (!url) return '';
183
184
  return url.replace(/([^:])\/+/g, '$1/').replace(/\/(\?|#|$)/, '$1');
184
185
  }
185
186
 
@@ -6,7 +6,7 @@ void PluginDropdown;
6
6
 
7
7
  const DEFAULT_UNIT_MAP = {
8
8
  text: {
9
- default: '13px',
9
+ default: '16px',
10
10
  list: [
11
11
  {
12
12
  title: 'XX-Small',
@@ -166,7 +166,7 @@ export function makePorts(
166
166
  formatAttrsTempCache: (attrs: any) => any;
167
167
  setOnShortcutKey: (v: any) => any;
168
168
  /**
169
- * @description Scrolls the editor view to the caret position after pressing `Enter`. (Ignored on mobile devices)
169
+ * @description Scrolls the editor view to the caret position after pressing `Enter`.
170
170
  * @param {Range} range Range object
171
171
  */
172
172
  enterScrollTo(range: Range): void;
@@ -406,7 +406,22 @@ export namespace DEFAULTS {
406
406
  * @property {boolean} [tabDisable=false] - Disables tab key input.
407
407
  * @property {string} [toolbar_width="auto"] - Toolbar width.
408
408
  * @property {?HTMLElement} [toolbar_container] - Container element for the toolbar.
409
- * @property {number} [toolbar_sticky=0] - Enables sticky toolbar with optional offset.
409
+ * @property {number|{top: number, offset: number}} [toolbar_sticky=0] - Enables sticky toolbar.
410
+ * - `number`: Sets the sticky top position (px). Use `-1` to disable sticky.
411
+ * - `{top, offset}`: `top` is the sticky position when the page header is visible.
412
+ * - `offset` is the sticky position when a virtual keyboard shifts the viewport (e.g., on tablets, touch devices).
413
+ * - When the virtual keyboard is active, `offset` replaces `top` so the toolbar doesn't leave a gap
414
+ * - for a page header that has scrolled out of view. Default `offset` is `0`.
415
+ * ```js
416
+ * // Basic usage — sticky at top with 0px offset
417
+ * toolbar_sticky: 0
418
+ *
419
+ * // Account for a 92px fixed/sticky site header
420
+ * toolbar_sticky: 92
421
+ *
422
+ * // 92px header on desktop, but 0px when virtual keyboard pushes the viewport
423
+ * toolbar_sticky: { top: 92, offset: 0 }
424
+ * ```
410
425
  * @property {boolean} [toolbar_hide=false] - Hides toolbar initially.
411
426
  * @property {Object} [subToolbar={}] - Sub-toolbar configuration. A secondary toolbar that appears on text selection.
412
427
  * @property {SunEditor.UI.ButtonList} [subToolbar.buttonList] - List of Sub-toolbar buttons, grouped by sub-arrays.
@@ -1201,9 +1216,29 @@ export type EditorBaseOptions = {
1201
1216
  */
1202
1217
  toolbar_container?: HTMLElement | null;
1203
1218
  /**
1204
- * - Enables sticky toolbar with optional offset.
1219
+ * - Enables sticky toolbar.
1220
+ * - `number`: Sets the sticky top position (px). Use `-1` to disable sticky.
1221
+ * - `{top, offset}`: `top` is the sticky position when the page header is visible.
1222
+ * - `offset` is the sticky position when a virtual keyboard shifts the viewport (e.g., on tablets, touch devices).
1223
+ * - When the virtual keyboard is active, `offset` replaces `top` so the toolbar doesn't leave a gap
1224
+ * - for a page header that has scrolled out of view. Default `offset` is `0`.
1225
+ * ```js
1226
+ * // Basic usage — sticky at top with 0px offset
1227
+ * toolbar_sticky: 0
1228
+ *
1229
+ * // Account for a 92px fixed/sticky site header
1230
+ * toolbar_sticky: 92
1231
+ *
1232
+ * // 92px header on desktop, but 0px when virtual keyboard pushes the viewport
1233
+ * toolbar_sticky: { top: 92, offset: 0 }
1234
+ * ```
1205
1235
  */
1206
- toolbar_sticky?: number;
1236
+ toolbar_sticky?:
1237
+ | number
1238
+ | {
1239
+ top: number;
1240
+ offset: number;
1241
+ };
1207
1242
  /**
1208
1243
  * - Hides toolbar initially.
1209
1244
  */
@@ -1513,7 +1548,18 @@ export type InternalBaseOptions = {
1513
1548
  };
1514
1549
  export type EditorInitOptions = EditorBaseOptions & PrivateBaseOptions & EditorFrameOptions;
1515
1550
  export type AllBaseOptions = EditorBaseOptions & PrivateBaseOptions & InternalBaseOptions;
1516
- export type TransformedOptionKeys = 'formatClosureBrLine' | 'formatBrLine' | 'formatLine' | 'formatClosureBlock' | 'formatBlock' | 'toolbar_width' | 'toolbar_container' | 'toolbar_sticky' | 'strictMode' | 'lineAttrReset';
1551
+ export type TransformedOptionKeys =
1552
+ | 'formatClosureBrLine'
1553
+ | 'formatBrLine'
1554
+ | 'formatLine'
1555
+ | 'formatClosureBlock'
1556
+ | 'formatBlock'
1557
+ | 'toolbar_width'
1558
+ | 'toolbar_container'
1559
+ | '_toolbar_sticky'
1560
+ | '_toolbar_sticky_offset'
1561
+ | 'strictMode'
1562
+ | 'lineAttrReset';
1517
1563
  export type StrictModeOptions = {
1518
1564
  /**
1519
1565
  * - Filters disallowed HTML tags (`elementWhitelist`/`elementBlacklist`)
@@ -1563,7 +1609,8 @@ export type TransformedOptions = {
1563
1609
  };
1564
1610
  toolbar_width: string;
1565
1611
  toolbar_container: HTMLElement | null;
1566
- toolbar_sticky: number;
1612
+ _toolbar_sticky: number;
1613
+ _toolbar_sticky_offset: number;
1567
1614
  strictMode: StrictModeOptions;
1568
1615
  lineAttrReset: string[];
1569
1616
  };