suneditor 3.0.0-rc.4 → 3.0.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.
Files changed (171) hide show
  1. package/README.md +4 -3
  2. package/dist/suneditor-contents.min.css +1 -1
  3. package/dist/suneditor.min.css +1 -1
  4. package/dist/suneditor.min.js +1 -1
  5. package/package.json +10 -6
  6. package/src/assets/design/color.css +14 -2
  7. package/src/assets/design/typography.css +5 -0
  8. package/src/assets/icons/defaultIcons.js +22 -4
  9. package/src/assets/suneditor-contents.css +1 -1
  10. package/src/assets/suneditor.css +312 -18
  11. package/src/core/config/eventManager.js +6 -9
  12. package/src/core/editor.js +1 -1
  13. package/src/core/event/actions/index.js +5 -0
  14. package/src/core/event/effects/keydown.registry.js +25 -0
  15. package/src/core/event/eventOrchestrator.js +69 -2
  16. package/src/core/event/handlers/handler_ww_mouse.js +1 -0
  17. package/src/core/event/rules/keydown.rule.backspace.js +9 -1
  18. package/src/core/kernel/coreKernel.js +4 -0
  19. package/src/core/kernel/store.js +2 -0
  20. package/src/core/logic/dom/char.js +11 -0
  21. package/src/core/logic/dom/format.js +22 -0
  22. package/src/core/logic/dom/html.js +126 -11
  23. package/src/core/logic/dom/nodeTransform.js +13 -0
  24. package/src/core/logic/dom/offset.js +100 -37
  25. package/src/core/logic/dom/selection.js +54 -22
  26. package/src/core/logic/panel/finder.js +982 -0
  27. package/src/core/logic/panel/menu.js +8 -6
  28. package/src/core/logic/panel/toolbar.js +112 -19
  29. package/src/core/logic/panel/viewer.js +214 -43
  30. package/src/core/logic/shell/_commandExecutor.js +7 -1
  31. package/src/core/logic/shell/commandDispatcher.js +1 -1
  32. package/src/core/logic/shell/component.js +5 -7
  33. package/src/core/logic/shell/history.js +24 -0
  34. package/src/core/logic/shell/shortcuts.js +3 -3
  35. package/src/core/logic/shell/ui.js +25 -26
  36. package/src/core/schema/frameContext.js +15 -1
  37. package/src/core/schema/options.js +180 -39
  38. package/src/core/section/constructor.js +61 -20
  39. package/src/core/section/documentType.js +2 -2
  40. package/src/events.js +12 -0
  41. package/src/helper/clipboard.js +1 -1
  42. package/src/helper/converter.js +15 -0
  43. package/src/helper/dom/domQuery.js +12 -0
  44. package/src/helper/dom/domUtils.js +26 -14
  45. package/src/helper/index.js +3 -0
  46. package/src/helper/markdown.js +876 -0
  47. package/src/interfaces/plugins.js +7 -5
  48. package/src/langs/ckb.js +9 -0
  49. package/src/langs/cs.js +9 -0
  50. package/src/langs/da.js +9 -0
  51. package/src/langs/de.js +9 -0
  52. package/src/langs/en.js +9 -0
  53. package/src/langs/es.js +9 -0
  54. package/src/langs/fa.js +9 -0
  55. package/src/langs/fr.js +9 -0
  56. package/src/langs/he.js +9 -0
  57. package/src/langs/hu.js +9 -0
  58. package/src/langs/it.js +9 -0
  59. package/src/langs/ja.js +9 -0
  60. package/src/langs/km.js +9 -0
  61. package/src/langs/ko.js +9 -0
  62. package/src/langs/lv.js +9 -0
  63. package/src/langs/nl.js +9 -0
  64. package/src/langs/pl.js +9 -0
  65. package/src/langs/pt_br.js +9 -0
  66. package/src/langs/ro.js +9 -0
  67. package/src/langs/ru.js +9 -0
  68. package/src/langs/se.js +9 -0
  69. package/src/langs/tr.js +9 -0
  70. package/src/langs/uk.js +9 -0
  71. package/src/langs/ur.js +9 -0
  72. package/src/langs/zh_cn.js +9 -0
  73. package/src/modules/contract/Browser.js +31 -1
  74. package/src/modules/contract/ColorPicker.js +6 -0
  75. package/src/modules/contract/Controller.js +77 -39
  76. package/src/modules/contract/Figure.js +57 -0
  77. package/src/modules/contract/Modal.js +6 -0
  78. package/src/modules/manager/ApiManager.js +53 -4
  79. package/src/modules/manager/FileManager.js +18 -1
  80. package/src/modules/ui/ModalAnchorEditor.js +35 -2
  81. package/src/modules/ui/SelectMenu.js +44 -12
  82. package/src/plugins/browser/fileBrowser.js +5 -2
  83. package/src/plugins/command/codeBlock.js +324 -0
  84. package/src/plugins/command/exportPDF.js +15 -3
  85. package/src/plugins/command/fileUpload.js +4 -1
  86. package/src/plugins/dropdown/backgroundColor.js +5 -1
  87. package/src/plugins/dropdown/blockStyle.js +8 -2
  88. package/src/plugins/dropdown/fontColor.js +5 -1
  89. package/src/plugins/dropdown/hr.js +6 -0
  90. package/src/plugins/dropdown/layout.js +4 -1
  91. package/src/plugins/dropdown/lineHeight.js +3 -0
  92. package/src/plugins/dropdown/paragraphStyle.js +5 -5
  93. package/src/plugins/dropdown/table/index.js +4 -1
  94. package/src/plugins/dropdown/table/render/table.html.js +1 -1
  95. package/src/plugins/dropdown/table/services/table.grid.js +16 -8
  96. package/src/plugins/dropdown/table/services/table.style.js +5 -9
  97. package/src/plugins/dropdown/template.js +3 -0
  98. package/src/plugins/dropdown/textStyle.js +5 -1
  99. package/src/plugins/field/mention.js +5 -1
  100. package/src/plugins/index.js +3 -0
  101. package/src/plugins/input/fontSize.js +10 -3
  102. package/src/plugins/modal/audio.js +7 -3
  103. package/src/plugins/modal/embed.js +23 -20
  104. package/src/plugins/modal/image/index.js +5 -1
  105. package/src/plugins/modal/math.js +7 -2
  106. package/src/plugins/modal/video/index.js +21 -4
  107. package/src/themes/cobalt.css +13 -4
  108. package/src/themes/cream.css +11 -2
  109. package/src/themes/dark.css +13 -4
  110. package/src/themes/midnight.css +13 -4
  111. package/src/typedef.js +4 -4
  112. package/types/assets/icons/defaultIcons.d.ts +12 -1
  113. package/types/assets/suneditor.css.d.ts +1 -1
  114. package/types/core/config/eventManager.d.ts +6 -8
  115. package/types/core/event/actions/index.d.ts +1 -0
  116. package/types/core/event/effects/keydown.registry.d.ts +2 -0
  117. package/types/core/event/eventOrchestrator.d.ts +2 -1
  118. package/types/core/kernel/coreKernel.d.ts +5 -0
  119. package/types/core/kernel/store.d.ts +5 -0
  120. package/types/core/logic/dom/char.d.ts +11 -0
  121. package/types/core/logic/dom/format.d.ts +22 -0
  122. package/types/core/logic/dom/html.d.ts +16 -0
  123. package/types/core/logic/dom/nodeTransform.d.ts +13 -0
  124. package/types/core/logic/dom/offset.d.ts +23 -2
  125. package/types/core/logic/dom/selection.d.ts +9 -3
  126. package/types/core/logic/panel/finder.d.ts +83 -0
  127. package/types/core/logic/panel/toolbar.d.ts +14 -1
  128. package/types/core/logic/panel/viewer.d.ts +22 -2
  129. package/types/core/logic/shell/shortcuts.d.ts +1 -1
  130. package/types/core/schema/frameContext.d.ts +22 -0
  131. package/types/core/schema/options.d.ts +362 -79
  132. package/types/events.d.ts +11 -0
  133. package/types/helper/converter.d.ts +15 -0
  134. package/types/helper/dom/domQuery.d.ts +12 -0
  135. package/types/helper/dom/domUtils.d.ts +23 -2
  136. package/types/helper/index.d.ts +5 -0
  137. package/types/helper/markdown.d.ts +27 -0
  138. package/types/interfaces/plugins.d.ts +7 -5
  139. package/types/langs/_Lang.d.ts +9 -0
  140. package/types/modules/contract/Browser.d.ts +36 -2
  141. package/types/modules/contract/ColorPicker.d.ts +6 -0
  142. package/types/modules/contract/Controller.d.ts +35 -1
  143. package/types/modules/contract/Figure.d.ts +57 -0
  144. package/types/modules/contract/Modal.d.ts +6 -0
  145. package/types/modules/manager/ApiManager.d.ts +26 -0
  146. package/types/modules/manager/FileManager.d.ts +17 -0
  147. package/types/modules/ui/ModalAnchorEditor.d.ts +41 -4
  148. package/types/modules/ui/SelectMenu.d.ts +40 -2
  149. package/types/plugins/browser/fileBrowser.d.ts +10 -4
  150. package/types/plugins/command/codeBlock.d.ts +53 -0
  151. package/types/plugins/command/fileUpload.d.ts +8 -2
  152. package/types/plugins/dropdown/backgroundColor.d.ts +10 -2
  153. package/types/plugins/dropdown/blockStyle.d.ts +14 -2
  154. package/types/plugins/dropdown/fontColor.d.ts +10 -2
  155. package/types/plugins/dropdown/hr.d.ts +12 -0
  156. package/types/plugins/dropdown/layout.d.ts +8 -2
  157. package/types/plugins/dropdown/lineHeight.d.ts +6 -0
  158. package/types/plugins/dropdown/paragraphStyle.d.ts +14 -3
  159. package/types/plugins/dropdown/table/index.d.ts +9 -3
  160. package/types/plugins/dropdown/template.d.ts +6 -0
  161. package/types/plugins/dropdown/textStyle.d.ts +10 -2
  162. package/types/plugins/field/mention.d.ts +10 -2
  163. package/types/plugins/index.d.ts +3 -0
  164. package/types/plugins/input/fontSize.d.ts +18 -4
  165. package/types/plugins/modal/audio.d.ts +14 -6
  166. package/types/plugins/modal/embed.d.ts +44 -38
  167. package/types/plugins/modal/image/index.d.ts +9 -1
  168. package/types/plugins/modal/link.d.ts +6 -2
  169. package/types/plugins/modal/math.d.ts +23 -5
  170. package/types/plugins/modal/video/index.d.ts +49 -9
  171. package/types/typedef.d.ts +5 -2
@@ -301,6 +301,8 @@ class NodeTransform {
301
301
  * @description Remove nested tags without other child nodes.
302
302
  * @param {Node} element Element object
303
303
  * @param {?(((current: Node) => boolean)|string)} [validation] Validation function / String(`tag1|tag2..`) / If `null`, all tags are applicable.
304
+ * @example
305
+ * editor.$.nodeTransform.mergeNestedTags(parentElement, (current) => current.nodeName === 'SPAN');
304
306
  */
305
307
  mergeNestedTags(element, validation) {
306
308
  if (typeof validation === 'string') {
@@ -419,6 +421,17 @@ class NodeTransform {
419
421
  * @param {SunEditor.NodeCollection} nodeArray An array of nodes to clone. The first node in the array will be the top-level parent.
420
422
  * @param {?(current: Node) => boolean} [validate] A validate function.
421
423
  * @returns {{ parent: Node, inner: Node }} An object containing the top-level parent node and the innermost child node.
424
+ * @example
425
+ * // [div, span, em] → <div><span><em></em></span></div> (cloned)
426
+ * const { parent, inner } = editor.$.nodeTransform.createNestedNode([div, span, em]);
427
+ * // parent = div (top), inner = em (innermost)
428
+ *
429
+ * // validate: skip nodes that fail the condition
430
+ * const { parent, inner } = editor.$.nodeTransform.createNestedNode(
431
+ * [div, span, em],
432
+ * (node) => node.nodeName !== 'SPAN'
433
+ * );
434
+ * // Result: <div><em></em></div> (SPAN excluded)
422
435
  */
423
436
  createNestedNode(nodeArray, validate) {
424
437
  if (typeof validate !== 'function') validate = () => true;
@@ -201,6 +201,7 @@ class Offset {
201
201
  }
202
202
 
203
203
  /**
204
+ * @deprecated
204
205
  * @description Gets the current editor-relative scroll offset.
205
206
  * @param {?Node} [node] Target element.
206
207
  * @returns {OffsetGlobalScrollInfo} Global scroll information.
@@ -357,41 +358,67 @@ class Offset {
357
358
  * @param {HTMLElement} e_container Element's root container
358
359
  * @param {HTMLElement} target Target element to position against
359
360
  * @param {HTMLElement} t_container Target's root container
361
+ * @param {Object} [opts] Options
362
+ * @param {boolean} [opts.preferUp=false] Open upward by default (for bottom toolbar)
360
363
  */
361
- setRelPosition(element, e_container, target, t_container) {
364
+ setRelPosition(element, e_container, target, t_container, { preferUp } = {}) {
362
365
  const isFixedContainer = /^fixed$/i.test(_w.getComputedStyle(t_container).position);
363
366
  const tGlobal = this.getGlobal(target);
364
367
 
365
368
  // top
366
369
  if (isFixedContainer) {
367
370
  element.style.position = 'fixed';
368
- element.style.top = `${tGlobal.fixedTop + tGlobal.height}px`;
371
+ if (preferUp) {
372
+ element.style.top = `${tGlobal.fixedTop - element.offsetHeight}px`;
373
+ } else {
374
+ element.style.top = `${tGlobal.fixedTop + tGlobal.height}px`;
375
+ }
369
376
  } else {
370
377
  element.style.position = '';
371
378
 
372
379
  const isSameContainer = t_container.contains(element);
373
380
  const containerTop = isSameContainer ? this.getGlobal(e_container).top : 0;
374
381
  const elHeight = element.offsetHeight;
375
- const scrollTop = this.getGlobalScroll().top;
382
+ const scrollTop = _w.scrollY;
376
383
  const bt = tGlobal.top;
377
384
 
378
- const menuHeight_bottom = getClientSize(_d).h - (containerTop - scrollTop + bt + target.offsetHeight);
379
- if (menuHeight_bottom < elHeight) {
380
- let menuTop = -1 * (elHeight - bt + 3);
381
- const insTop = containerTop - scrollTop + menuTop;
382
- const menuHeight_top = elHeight + (insTop < 0 ? insTop : 0);
383
-
384
- if (menuHeight_top > menuHeight_bottom) {
385
- element.style.height = `${menuHeight_top}px`;
386
- menuTop = -1 * (menuHeight_top - bt + 3);
385
+ if (preferUp) {
386
+ // Try to open above
387
+ const menuHeight_top = containerTop - scrollTop + bt;
388
+ if (menuHeight_top < elHeight) {
389
+ // Not enough space above try below
390
+ const menuHeight_bottom = getClientSize(_d).h - (containerTop - scrollTop + bt + target.offsetHeight);
391
+ if (menuHeight_bottom >= elHeight) {
392
+ element.style.top = `${bt + target.offsetHeight}px`;
393
+ } else if (menuHeight_bottom > menuHeight_top) {
394
+ element.style.height = `${menuHeight_bottom}px`;
395
+ element.style.top = `${bt + target.offsetHeight}px`;
396
+ } else {
397
+ element.style.height = `${menuHeight_top}px`;
398
+ element.style.top = `${-1 * (menuHeight_top - bt + 3)}px`;
399
+ }
387
400
  } else {
388
- element.style.height = `${menuHeight_bottom}px`;
389
- menuTop = bt + target.offsetHeight;
401
+ element.style.top = `${bt - elHeight}px`;
390
402
  }
391
-
392
- element.style.top = `${menuTop}px`;
393
403
  } else {
394
- element.style.top = `${bt + target.offsetHeight}px`;
404
+ const menuHeight_bottom = getClientSize(_d).h - (containerTop - scrollTop + bt + target.offsetHeight);
405
+ if (menuHeight_bottom < elHeight) {
406
+ let menuTop = -1 * (elHeight - bt + 3);
407
+ const insTop = containerTop - scrollTop + menuTop;
408
+ const menuHeight_top = elHeight + (insTop < 0 ? insTop : 0);
409
+
410
+ if (menuHeight_top > menuHeight_bottom) {
411
+ element.style.height = `${menuHeight_top}px`;
412
+ menuTop = -1 * (menuHeight_top - bt + 3);
413
+ } else {
414
+ element.style.height = `${menuHeight_bottom}px`;
415
+ menuTop = bt + target.offsetHeight;
416
+ }
417
+
418
+ element.style.top = `${menuTop}px`;
419
+ } else {
420
+ element.style.top = `${bt + target.offsetHeight}px`;
421
+ }
395
422
  }
396
423
  }
397
424
 
@@ -425,15 +452,20 @@ class Offset {
425
452
  * @param {HTMLElement} target Target element
426
453
  * @param {Object} params Position parameters
427
454
  * @param {boolean} [params.isWWTarget=false] Whether the target is within the editor's WYSIWYG area
428
- * @param {{left:number, top:number}} [params.addOffset={left:0, top:0}] Additional offset
455
+ * @param {{left:number, right:number, top:number}} [params.addOffset={left:0, right:0, top:0}] Additional offset
429
456
  * @param {"bottom"|"top"} [params.position="bottom"] Position ('bottom'|'top')
430
457
  * @param {*} params.inst Instance object of caller
431
458
  * @param {HTMLElement} [params.sibling=null] The sibling controller element
432
459
  * @returns {{position: "top" | "bottom"} | undefined} Success -> {position: current position}
460
+ * @example
461
+ * const result = editor.$.offset.setAbsPosition(controller, targetElement, {
462
+ * position: 'bottom', inst: this, addOffset: { left: 0, right: 0, top: 0 }
463
+ * });
433
464
  */
434
465
  setAbsPosition(element, target, params) {
435
466
  const addOffset = {
436
467
  left: 0,
468
+ right: 0,
437
469
  top: 0,
438
470
  ...params.addOffset,
439
471
  };
@@ -478,7 +510,9 @@ class Offset {
478
510
  const { rmt, rmb, bMargin, rt } = this.#getVMargin(tmtw, tmbw, toolbarH, clientSize, targetRect, isTextSelection, isToolbarTarget);
479
511
  if ((isWWTarget && (rmb - statusBarH + targetH <= 0 || rmt + rt + targetH - (this.#$.toolbar.isSticky && isInlineTarget ? toolbarH : 0) <= 0)) || rmt + targetH < 0) return;
480
512
 
481
- const isSticky = this.#$.toolbar.isSticky && this.#context.get('toolbar_main').style.display !== 'none' && (!headLess || this.#frameContext.get('topArea').getBoundingClientRect().top <= th);
513
+ const topAreaRect = this.#frameContext.get('topArea').getBoundingClientRect();
514
+ const isStickyVisible = this.#store.mode.isBottom ? topAreaRect.bottom >= _w.innerHeight - th : topAreaRect.top <= th;
515
+ const isSticky = this.#$.toolbar.isSticky && this.#context.get('toolbar_main').style.display !== 'none' && (!headLess || isStickyVisible);
482
516
  let t = addOffset.top;
483
517
  let y = 0;
484
518
  let arrowDir = '';
@@ -504,7 +538,7 @@ class Offset {
504
538
  else {
505
539
  arrowDir = 'down';
506
540
  t += targetRect.top - elH - ah + wScrollY;
507
- y = (isSticky ? targetRect.top - toolbarH : rmt) - elH - ah;
541
+ y = (isSticky && !this.#store.mode.isBottom ? targetRect.top - toolbarH : rmt) - elH - ah;
508
542
  // change to [bottom] position
509
543
  if (y - siblingH < 0) {
510
544
  arrowDir = 'up';
@@ -536,7 +570,7 @@ class Offset {
536
570
  arrow.style.right = '';
537
571
  }
538
572
 
539
- let l = addOffset.left;
573
+ let l = addOffset.left || (addOffset.right ? (isLTR ? addOffset.right - element.offsetWidth : element.offsetWidth - addOffset.right) : 0);
540
574
  let x = 0;
541
575
  let ax = 0;
542
576
  let awLimit = 0;
@@ -591,6 +625,9 @@ class Offset {
591
625
  * @param {"bottom"|"top"} [options.position="bottom"] Position ('bottom'|'top')
592
626
  * @param {number} [options.addTop=0] Additional top offset
593
627
  * @returns {boolean} Success / Failure
628
+ * @example
629
+ * const success = editor.$.offset.setRangePosition(toolbar, null, { position: 'bottom', addTop: 0 });
630
+ * if (!success) toolbar.style.display = 'none';
594
631
  */
595
632
  setRangePosition(element, range, { position, addTop } = {}) {
596
633
  element.style.top = '-10000px';
@@ -604,12 +641,13 @@ class Offset {
604
641
 
605
642
  const isFullScreen = this.#frameContext.get('isFullScreen');
606
643
  const topArea = this.#frameContext.get('topArea');
644
+ const isInCarrier = this.#carrierWrapper.contains(element);
607
645
  const rects = rectsObj.rects;
608
646
  const scrollLeft = isFullScreen ? 0 : rectsObj.scrollLeft;
609
647
  const scrollTop = isFullScreen ? 0 : rectsObj.scrollTop;
610
- const editorWidth = topArea.offsetWidth;
648
+ const editorWidth = isInCarrier ? getClientSize(_d).w : topArea.offsetWidth;
611
649
  const offsets = this.getGlobal(topArea);
612
- const editorLeft = offsets.left;
650
+ const editorLeft = isInCarrier ? 0 : offsets.left;
613
651
  const toolbarWidth = element.offsetWidth;
614
652
  const toolbarHeight = element.offsetHeight;
615
653
 
@@ -733,33 +771,58 @@ class Offset {
733
771
  bMargin = clientSize.h - targetRect.bottom;
734
772
  const editorOffset = this.getGlobal();
735
773
 
774
+ const isBottom = this.#store.mode.isBottom;
736
775
  if (!isTextSelection) {
737
776
  const emt = editorOffset.fixedTop > 0 ? editorOffset.fixedTop : 0;
738
777
  const emb = _w.innerHeight - (editorOffset.fixedTop + editorOffset.height);
739
778
  rt = !isToolbarTarget && (this.#$.toolbar.isSticky || !this.#$.toolbar.isBalloonMode) ? toolbarH : 0;
740
- rmt = tMargin - (!isToolbarTarget ? emt : 0) - rt;
741
- rmb = bMargin - (emb > 0 ? emb : 0);
779
+ if (isBottom) {
780
+ rmt = tMargin - (!isToolbarTarget ? emt : 0);
781
+ rmb = bMargin - (emb > 0 ? emb : 0) - rt;
782
+ } else {
783
+ rmt = tMargin - (!isToolbarTarget ? emt : 0) - rt;
784
+ rmb = bMargin - (emb > 0 ? emb : 0);
785
+ }
742
786
  } else {
743
787
  rt = !isToolbarTarget && !this.#$.toolbar.isSticky && !this.#options.get('toolbar_container') ? toolbarH : 0;
744
788
  const wst = !isIframe ? editorOffset.top - _w.scrollY + rt : 0;
745
789
  const wsb = !isIframe ? this.#store.get('currentViewportHeight') - (editorOffset.top + editorOffset.height - _w.scrollY) : 0;
746
790
  let st = wst;
747
- if (toolbarH > wst) {
748
- if (this.#$.toolbar.isSticky) {
749
- st = toolbarH;
791
+ let sb = wsb;
792
+ if (isBottom) {
793
+ if (toolbarH > wsb) {
794
+ if (this.#$.toolbar.isSticky) {
795
+ sb = toolbarH;
796
+ } else {
797
+ sb = wsb + toolbarH;
798
+ }
799
+ } else if (this.#options.get('toolbar_container') && !this.#$.toolbar.isSticky) {
800
+ toolbarH = 0;
750
801
  } else {
751
- st = wst + toolbarH;
802
+ sb = wsb + toolbarH;
752
803
  }
753
- } else if (this.#options.get('toolbar_container') && !this.#$.toolbar.isSticky) {
754
- toolbarH = 0;
804
+
805
+ rmt = targetRect.top - (wwRects.top - wst);
806
+ rmb = wwRects.bottom - (targetRect.bottom - sb) + toolbarH;
807
+ rmb = rmb > 0 ? rmb : rmb - toolbarH;
755
808
  } else {
756
- st = wst + toolbarH;
757
- }
809
+ if (toolbarH > wst) {
810
+ if (this.#$.toolbar.isSticky) {
811
+ st = toolbarH;
812
+ } else {
813
+ st = wst + toolbarH;
814
+ }
815
+ } else if (this.#options.get('toolbar_container') && !this.#$.toolbar.isSticky) {
816
+ toolbarH = 0;
817
+ } else {
818
+ st = wst + toolbarH;
819
+ }
758
820
 
759
- rmt = targetRect.top - (wwRects.top - st) + toolbarH;
760
- rmb = wwRects.bottom - (targetRect.bottom - wsb);
761
- // display margin
762
- rmt = rmt > 0 ? rmt : rmt - toolbarH;
821
+ rmt = targetRect.top - (wwRects.top - st) + toolbarH;
822
+ rmb = wwRects.bottom - (targetRect.bottom - wsb);
823
+ // display margin
824
+ rmt = rmt > 0 ? rmt : rmt - toolbarH;
825
+ }
763
826
  }
764
827
 
765
828
  return {
@@ -210,6 +210,9 @@ class Selection_ {
210
210
  * - If there is no next sibling but a previous sibling exists, it returns the previous sibling with an offset of 1.
211
211
  * @param {Node} target Target node whose neighboring range is to be determined.
212
212
  * @returns {{container: Node, offset: number}|null} An object containing the nearest container node and its offset.
213
+ * @example
214
+ * const nearRange = editor.$.selection.getNearRange(targetNode);
215
+ * if (nearRange) editor.$.selection.setRange(nearRange.container, nearRange.offset, nearRange.container, nearRange.offset);
213
216
  */
214
217
  getNearRange(target) {
215
218
  const next = target.nextSibling;
@@ -249,6 +252,9 @@ class Selection_ {
249
252
  /**
250
253
  * @description Get current select node
251
254
  * @returns {HTMLElement|Text}
255
+ * @example
256
+ * const node = editor.$.selection.getNode();
257
+ * const line = editor.$.format.getLine(node);
252
258
  */
253
259
  getNode() {
254
260
  if (!this.#frameContext.get('wysiwyg').contains(this.selectionNode)) this.init();
@@ -286,10 +292,9 @@ class Selection_ {
286
292
  getRects(target, position) {
287
293
  const targetAbs = dom.check.isElement(/** @type {Node} */ (target)) ? _w.getComputedStyle(target).position === 'absolute' : false;
288
294
  target = /** @type {Range} */ (!target || dom.check.isText(/** @type {Node} */ (target)) ? this.getRange() : target);
289
- const globalScroll = this.#$.offset.getGlobalScroll();
290
295
  let isStartPosition = position === 'start';
291
- let scrollLeft = globalScroll.left;
292
- let scrollTop = globalScroll.top;
296
+ let scrollLeft = _w.scrollX;
297
+ let scrollTop = _w.scrollY;
293
298
 
294
299
  let rects = /** @type {*} */ (target).getClientRects();
295
300
  rects = rects[isStartPosition ? 0 : rects.length - 1];
@@ -373,7 +378,7 @@ class Selection_ {
373
378
  /**
374
379
  * @description Scroll to the corresponding selection or range position.
375
380
  * @param {Selection|Range|Node} ref selection or range object
376
- * @param {Object<string, *>} [scrollOption] option of scrollTo
381
+ * @param {ScrollIntoViewOptions & {noFocus?: boolean}} [scrollOption] Scroll options. Extends `ScrollIntoViewOptions` (`behavior`, `block`, `inline`) with `noFocus` to prevent focus change.
377
382
  * @example
378
383
  * // Scroll to current selection smoothly
379
384
  * editor.selection.scrollTo(editor.selection.get());
@@ -389,10 +394,18 @@ class Selection_ {
389
394
  * });
390
395
  */
391
396
  scrollTo(ref, scrollOption) {
397
+ const noFocus = scrollOption?.noFocus;
398
+
392
399
  if (this.#instanceCheck.isSelection(ref)) {
393
400
  ref = ref.getRangeAt(0);
394
401
  } else if (this.#instanceCheck.isNode(ref)) {
395
- ref = this.setRange(ref, 1, ref, 1);
402
+ if (noFocus) {
403
+ const range = this.#frameContext.get('_wd').createRange();
404
+ range.selectNodeContents(ref);
405
+ ref = range;
406
+ } else {
407
+ ref = this.setRange(ref, 1, ref, 1);
408
+ }
396
409
  } else if (typeof ref?.startContainer === 'undefined') {
397
410
  console.warn('[SUNEDITOR.html.scrollTo.warn] "selectionRange" must be Selection or Range or Node object.', ref);
398
411
  }
@@ -401,6 +414,7 @@ class Selection_ {
401
414
  if (!el) return;
402
415
 
403
416
  scrollOption = { behavior: 'smooth', block: 'nearest', inline: 'nearest', ...scrollOption };
417
+ delete scrollOption.noFocus;
404
418
 
405
419
  const ww = this.#frameContext.get('_ww');
406
420
  const wwFrame = this.#frameContext.get('wysiwygFrame');
@@ -408,6 +422,7 @@ class Selection_ {
408
422
  const isAutoHeight = !this.#store.get('isScrollable')(this.#frameContext);
409
423
  const viewportHeight = this.#store.get('currentViewportHeight');
410
424
  const scrollY = isAutoHeight ? _w.scrollY : isIframe ? ww.scrollY : wwFrame.scrollTop;
425
+ const isBottom = this.#store.mode.isBottom;
411
426
  const realToolbarHeight = this.#context.get('toolbar_main').offsetHeight;
412
427
  const toolbarHeight = this.#$.toolbar.isSticky ? realToolbarHeight : 0;
413
428
  const positionToolbarHeight = this.#$.toolbar.isSticky ? toolbarHeight + this.#options.get('toolbar_sticky') : toolbarHeight;
@@ -417,8 +432,18 @@ class Selection_ {
417
432
  el?.scrollIntoView(scrollOption);
418
433
 
419
434
  if (scrollOption?.behavior === 'auto' && scrollY !== _w.scrollY) {
420
- if (positionToolbarHeight && scrollY > _w.scrollY) {
421
- _w.scrollBy(0, -positionToolbarHeight);
435
+ if (positionToolbarHeight) {
436
+ if (isBottom) {
437
+ // bottom toolbar covers the bottom — scroll down more so the element clears the toolbar
438
+ if (scrollY < _w.scrollY) {
439
+ _w.scrollBy(0, positionToolbarHeight);
440
+ }
441
+ } else {
442
+ // top toolbar covers the top — scroll up more so the element clears the toolbar
443
+ if (scrollY > _w.scrollY) {
444
+ _w.scrollBy(0, -positionToolbarHeight);
445
+ }
446
+ }
422
447
  } else if (isAutoHeight) {
423
448
  _w.scrollBy(0, statusbarHeight);
424
449
  }
@@ -429,55 +454,62 @@ class Selection_ {
429
454
 
430
455
  // --- When there is no upper scroll and it is an iframe ---
431
456
  const PADDING = this.#scrollMargin;
432
- const viewHeight = isAutoHeight ? viewportHeight : wwFrame.offsetHeight;
433
- const elH = el.offsetHeight || 0;
457
+ // Reduce effective viewport height by toolbar+offset when bottom toolbar is sticky
458
+ const viewHeight = isAutoHeight ? viewportHeight - (isBottom ? positionToolbarHeight : 0) : wwFrame.offsetHeight;
459
+
460
+ // Use range rect for accurate height — el.offsetHeight includes nested children (e.g. nested lists)
461
+ const refRect = ref?.getBoundingClientRect?.();
462
+ const elH = (refRect?.height > 0 ? refRect.height : el.offsetHeight) || 0;
434
463
 
435
464
  const behavior = scrollOption?.behavior;
465
+ const topToolbarH = isBottom ? 0 : positionToolbarHeight;
436
466
  if (isAutoHeight) {
437
467
  if (isIframe) {
438
468
  const rect = this.getRects(ref, 'end').rects;
439
- const topMargin = rect.top + elH - positionToolbarHeight;
469
+ const topMargin = rect.top + elH - topToolbarH;
440
470
  const bottomMargin = viewHeight - PADDING - (rect.top + elH);
441
471
  if (topMargin >= 0 && bottomMargin >= 0) return;
442
472
 
443
473
  const newScrollTop = scrollY - (topMargin < 0 ? -(topMargin - PADDING) : bottomMargin);
444
474
  _w.scrollTo({
445
- top: newScrollTop < scrollY ? newScrollTop - positionToolbarHeight : newScrollTop,
475
+ top: newScrollTop < scrollY ? newScrollTop - topToolbarH : newScrollTop,
446
476
  behavior,
447
477
  });
448
478
  } else {
449
479
  const rect = this.#$.offset.getGlobal(el);
450
480
  const scrollMargin = viewHeight + scrollY - rect.top - elH;
451
481
 
452
- if (scrollMargin - PADDING > 0 && viewHeight > scrollMargin + PADDING + positionToolbarHeight) return;
482
+ if (scrollMargin - PADDING > 0 && viewHeight > scrollMargin + PADDING + topToolbarH) return;
453
483
 
454
484
  const newScrollTop = scrollMargin <= PADDING ? scrollY - scrollMargin + PADDING + statusbarHeight : scrollY - scrollMargin + (viewHeight - elH - PADDING);
455
485
  _w.scrollTo({
456
- top: newScrollTop < scrollY ? newScrollTop - positionToolbarHeight : newScrollTop,
486
+ top: newScrollTop < scrollY ? newScrollTop - topToolbarH : newScrollTop,
457
487
  behavior,
458
488
  });
459
489
  }
460
490
  } else {
461
- // local scroll
462
- const { rects } = this.getRects(el, 'start');
463
- const { top } = this.#$.offset.getLocal(el);
464
- const innerTop = top < 0 && rects.top < 0 ? top : rects.top;
491
+ // local scroll — use range rect for accurate position (el.getBoundingClientRect includes nested children)
492
+ const hasRefRect = refRect?.height > 0;
493
+ const targetTop = hasRefRect ? refRect.top : el.getBoundingClientRect().top;
494
+ const innerTop = isIframe ? targetTop : targetTop - wwFrame.getBoundingClientRect().top;
465
495
 
466
496
  const keepLocalScroll = innerTop - PADDING > 0 && innerTop + PADDING <= viewHeight;
467
- const rectScroll = innerTop - PADDING > 0 ? innerTop + PADDING - viewHeight : innerTop - (toolbarHeight + elH);
497
+ const rectScroll = isBottom ? (innerTop - PADDING > 0 ? innerTop + PADDING - viewHeight + toolbarHeight : innerTop - elH) : innerTop - PADDING > 0 ? innerTop + PADDING - viewHeight : innerTop - (toolbarHeight + elH);
468
498
  let newScrollTop = scrollY + rectScroll;
469
499
 
470
500
  // frame scroll
471
501
  const gy = _w.scrollY;
472
502
  const globalRect = this.#$.offset.getGlobal();
473
- const topMargin = gy - globalRect.top + realToolbarHeight;
474
- const bottomMargin = globalRect.top + globalRect.height - (gy + viewportHeight) + realToolbarHeight;
503
+ const topToolbarFrame = isBottom ? 0 : realToolbarHeight;
504
+ const bottomToolbarFrame = isBottom ? realToolbarHeight : 0;
505
+ const topMargin = gy - globalRect.top + topToolbarFrame;
506
+ const bottomMargin = globalRect.top + globalRect.height - (gy + viewportHeight) + bottomToolbarFrame;
475
507
 
476
508
  // set frame scroll
477
509
  if (topMargin > 0) {
478
510
  const newFrameY = (keepLocalScroll ? innerTop : innerTop + scrollY - newScrollTop) - elH - PADDING - topMargin;
479
511
  if (newFrameY < 0) {
480
- newScrollTop += realToolbarHeight;
512
+ newScrollTop += topToolbarFrame;
481
513
  _w.scrollTo({
482
514
  top: gy + newFrameY,
483
515
  behavior: 'smooth',
@@ -487,7 +519,7 @@ class Selection_ {
487
519
  if (bottomMargin > 0) {
488
520
  const newFrameY = (keepLocalScroll ? innerTop : innerTop + scrollY - newScrollTop) + elH + PADDING - (globalRect.height - bottomMargin);
489
521
  if (newFrameY > 0) {
490
- newScrollTop += statusbarHeight;
522
+ newScrollTop += isBottom ? bottomToolbarFrame : statusbarHeight;
491
523
  _w.scrollTo({
492
524
  top: gy + newFrameY,
493
525
  behavior: 'smooth',