suneditor 3.0.0-rc.5 → 3.0.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 (118) hide show
  1. package/README.md +3 -2
  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 +3 -3
  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/html.js +110 -11
  21. package/src/core/logic/dom/offset.js +89 -35
  22. package/src/core/logic/dom/selection.js +46 -19
  23. package/src/core/logic/panel/finder.js +982 -0
  24. package/src/core/logic/panel/menu.js +8 -6
  25. package/src/core/logic/panel/toolbar.js +112 -19
  26. package/src/core/logic/panel/viewer.js +214 -43
  27. package/src/core/logic/shell/_commandExecutor.js +7 -1
  28. package/src/core/logic/shell/commandDispatcher.js +1 -1
  29. package/src/core/logic/shell/component.js +5 -7
  30. package/src/core/logic/shell/history.js +24 -0
  31. package/src/core/logic/shell/shortcuts.js +5 -3
  32. package/src/core/logic/shell/ui.js +25 -26
  33. package/src/core/schema/frameContext.js +15 -1
  34. package/src/core/schema/options.js +75 -16
  35. package/src/core/section/constructor.js +62 -21
  36. package/src/core/section/documentType.js +1 -1
  37. package/src/events.js +12 -0
  38. package/src/helper/clipboard.js +1 -1
  39. package/src/helper/dom/domUtils.js +5 -14
  40. package/src/helper/index.js +3 -0
  41. package/src/helper/markdown.js +876 -0
  42. package/src/langs/ckb.js +9 -0
  43. package/src/langs/cs.js +9 -0
  44. package/src/langs/da.js +9 -0
  45. package/src/langs/de.js +9 -0
  46. package/src/langs/en.js +9 -0
  47. package/src/langs/es.js +9 -0
  48. package/src/langs/fa.js +9 -0
  49. package/src/langs/fr.js +9 -0
  50. package/src/langs/he.js +9 -0
  51. package/src/langs/hu.js +9 -0
  52. package/src/langs/it.js +9 -0
  53. package/src/langs/ja.js +9 -0
  54. package/src/langs/km.js +9 -0
  55. package/src/langs/ko.js +9 -0
  56. package/src/langs/lv.js +9 -0
  57. package/src/langs/nl.js +9 -0
  58. package/src/langs/pl.js +9 -0
  59. package/src/langs/pt_br.js +9 -0
  60. package/src/langs/ro.js +9 -0
  61. package/src/langs/ru.js +9 -0
  62. package/src/langs/se.js +9 -0
  63. package/src/langs/tr.js +9 -0
  64. package/src/langs/uk.js +9 -0
  65. package/src/langs/ur.js +9 -0
  66. package/src/langs/zh_cn.js +9 -0
  67. package/src/modules/contract/Controller.js +50 -39
  68. package/src/modules/manager/ApiManager.js +27 -4
  69. package/src/modules/manager/FileManager.js +1 -1
  70. package/src/modules/ui/SelectMenu.js +22 -11
  71. package/src/plugins/command/codeBlock.js +324 -0
  72. package/src/plugins/command/exportPDF.js +15 -3
  73. package/src/plugins/dropdown/blockStyle.js +1 -1
  74. package/src/plugins/dropdown/paragraphStyle.js +1 -2
  75. package/src/plugins/dropdown/table/render/table.html.js +1 -1
  76. package/src/plugins/dropdown/table/services/table.grid.js +16 -8
  77. package/src/plugins/dropdown/table/services/table.style.js +5 -9
  78. package/src/plugins/index.js +3 -0
  79. package/src/plugins/input/fontSize.js +4 -2
  80. package/src/plugins/modal/audio.js +2 -1
  81. package/src/plugins/modal/image/index.js +2 -1
  82. package/src/plugins/modal/math.js +2 -1
  83. package/src/plugins/modal/video/index.js +2 -1
  84. package/src/themes/cobalt.css +13 -4
  85. package/src/themes/cream.css +44 -35
  86. package/src/themes/dark.css +13 -4
  87. package/src/themes/midnight.css +13 -4
  88. package/src/typedef.js +4 -4
  89. package/types/assets/icons/defaultIcons.d.ts +12 -1
  90. package/types/core/config/eventManager.d.ts +6 -8
  91. package/types/core/event/actions/index.d.ts +1 -0
  92. package/types/core/event/effects/keydown.registry.d.ts +2 -0
  93. package/types/core/event/eventOrchestrator.d.ts +2 -1
  94. package/types/core/kernel/coreKernel.d.ts +5 -0
  95. package/types/core/kernel/store.d.ts +5 -0
  96. package/types/core/logic/dom/offset.d.ts +16 -3
  97. package/types/core/logic/dom/selection.d.ts +3 -3
  98. package/types/core/logic/panel/finder.d.ts +83 -0
  99. package/types/core/logic/panel/toolbar.d.ts +14 -1
  100. package/types/core/logic/panel/viewer.d.ts +22 -2
  101. package/types/core/logic/shell/shortcuts.d.ts +1 -1
  102. package/types/core/schema/frameContext.d.ts +22 -0
  103. package/types/core/schema/options.d.ts +153 -31
  104. package/types/events.d.ts +11 -0
  105. package/types/helper/dom/domUtils.d.ts +2 -2
  106. package/types/helper/index.d.ts +5 -0
  107. package/types/helper/markdown.d.ts +27 -0
  108. package/types/langs/_Lang.d.ts +9 -0
  109. package/types/modules/contract/Controller.d.ts +8 -1
  110. package/types/modules/ui/SelectMenu.d.ts +12 -0
  111. package/types/plugins/command/codeBlock.d.ts +53 -0
  112. package/types/plugins/index.d.ts +3 -0
  113. package/types/plugins/input/fontSize.d.ts +6 -2
  114. package/types/plugins/modal/audio.d.ts +4 -2
  115. package/types/plugins/modal/image/index.d.ts +3 -1
  116. package/types/plugins/modal/math.d.ts +3 -1
  117. package/types/plugins/modal/video/index.d.ts +3 -1
  118. package/types/typedef.d.ts +5 -2
@@ -358,15 +358,21 @@ class Offset {
358
358
  * @param {HTMLElement} e_container Element's root container
359
359
  * @param {HTMLElement} target Target element to position against
360
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)
361
363
  */
362
- setRelPosition(element, e_container, target, t_container) {
364
+ setRelPosition(element, e_container, target, t_container, { preferUp } = {}) {
363
365
  const isFixedContainer = /^fixed$/i.test(_w.getComputedStyle(t_container).position);
364
366
  const tGlobal = this.getGlobal(target);
365
367
 
366
368
  // top
367
369
  if (isFixedContainer) {
368
370
  element.style.position = 'fixed';
369
- 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
+ }
370
376
  } else {
371
377
  element.style.position = '';
372
378
 
@@ -376,23 +382,43 @@ class Offset {
376
382
  const scrollTop = _w.scrollY;
377
383
  const bt = tGlobal.top;
378
384
 
379
- const menuHeight_bottom = getClientSize(_d).h - (containerTop - scrollTop + bt + target.offsetHeight);
380
- if (menuHeight_bottom < elHeight) {
381
- let menuTop = -1 * (elHeight - bt + 3);
382
- const insTop = containerTop - scrollTop + menuTop;
383
- const menuHeight_top = elHeight + (insTop < 0 ? insTop : 0);
384
-
385
- if (menuHeight_top > menuHeight_bottom) {
386
- element.style.height = `${menuHeight_top}px`;
387
- 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
+ }
388
400
  } else {
389
- element.style.height = `${menuHeight_bottom}px`;
390
- menuTop = bt + target.offsetHeight;
401
+ element.style.top = `${bt - elHeight}px`;
391
402
  }
392
-
393
- element.style.top = `${menuTop}px`;
394
403
  } else {
395
- 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
+ }
396
422
  }
397
423
  }
398
424
 
@@ -426,19 +452,20 @@ class Offset {
426
452
  * @param {HTMLElement} target Target element
427
453
  * @param {Object} params Position parameters
428
454
  * @param {boolean} [params.isWWTarget=false] Whether the target is within the editor's WYSIWYG area
429
- * @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
430
456
  * @param {"bottom"|"top"} [params.position="bottom"] Position ('bottom'|'top')
431
457
  * @param {*} params.inst Instance object of caller
432
458
  * @param {HTMLElement} [params.sibling=null] The sibling controller element
433
459
  * @returns {{position: "top" | "bottom"} | undefined} Success -> {position: current position}
434
460
  * @example
435
461
  * const result = editor.$.offset.setAbsPosition(controller, targetElement, {
436
- * position: 'bottom', inst: this, addOffset: { left: 0, top: 0 }
462
+ * position: 'bottom', inst: this, addOffset: { left: 0, right: 0, top: 0 }
437
463
  * });
438
464
  */
439
465
  setAbsPosition(element, target, params) {
440
466
  const addOffset = {
441
467
  left: 0,
468
+ right: 0,
442
469
  top: 0,
443
470
  ...params.addOffset,
444
471
  };
@@ -483,7 +510,9 @@ class Offset {
483
510
  const { rmt, rmb, bMargin, rt } = this.#getVMargin(tmtw, tmbw, toolbarH, clientSize, targetRect, isTextSelection, isToolbarTarget);
484
511
  if ((isWWTarget && (rmb - statusBarH + targetH <= 0 || rmt + rt + targetH - (this.#$.toolbar.isSticky && isInlineTarget ? toolbarH : 0) <= 0)) || rmt + targetH < 0) return;
485
512
 
486
- 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);
487
516
  let t = addOffset.top;
488
517
  let y = 0;
489
518
  let arrowDir = '';
@@ -509,7 +538,7 @@ class Offset {
509
538
  else {
510
539
  arrowDir = 'down';
511
540
  t += targetRect.top - elH - ah + wScrollY;
512
- y = (isSticky ? targetRect.top - toolbarH : rmt) - elH - ah;
541
+ y = (isSticky && !this.#store.mode.isBottom ? targetRect.top - toolbarH : rmt) - elH - ah;
513
542
  // change to [bottom] position
514
543
  if (y - siblingH < 0) {
515
544
  arrowDir = 'up';
@@ -541,7 +570,7 @@ class Offset {
541
570
  arrow.style.right = '';
542
571
  }
543
572
 
544
- let l = addOffset.left;
573
+ let l = addOffset.left || (addOffset.right ? (isLTR ? addOffset.right - element.offsetWidth : element.offsetWidth - addOffset.right) : 0);
545
574
  let x = 0;
546
575
  let ax = 0;
547
576
  let awLimit = 0;
@@ -742,33 +771,58 @@ class Offset {
742
771
  bMargin = clientSize.h - targetRect.bottom;
743
772
  const editorOffset = this.getGlobal();
744
773
 
774
+ const isBottom = this.#store.mode.isBottom;
745
775
  if (!isTextSelection) {
746
776
  const emt = editorOffset.fixedTop > 0 ? editorOffset.fixedTop : 0;
747
777
  const emb = _w.innerHeight - (editorOffset.fixedTop + editorOffset.height);
748
778
  rt = !isToolbarTarget && (this.#$.toolbar.isSticky || !this.#$.toolbar.isBalloonMode) ? toolbarH : 0;
749
- rmt = tMargin - (!isToolbarTarget ? emt : 0) - rt;
750
- 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
+ }
751
786
  } else {
752
787
  rt = !isToolbarTarget && !this.#$.toolbar.isSticky && !this.#options.get('toolbar_container') ? toolbarH : 0;
753
788
  const wst = !isIframe ? editorOffset.top - _w.scrollY + rt : 0;
754
789
  const wsb = !isIframe ? this.#store.get('currentViewportHeight') - (editorOffset.top + editorOffset.height - _w.scrollY) : 0;
755
790
  let st = wst;
756
- if (toolbarH > wst) {
757
- if (this.#$.toolbar.isSticky) {
758
- 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;
759
801
  } else {
760
- st = wst + toolbarH;
802
+ sb = wsb + toolbarH;
761
803
  }
762
- } else if (this.#options.get('toolbar_container') && !this.#$.toolbar.isSticky) {
763
- 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;
764
808
  } else {
765
- st = wst + toolbarH;
766
- }
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
+ }
767
820
 
768
- rmt = targetRect.top - (wwRects.top - st) + toolbarH;
769
- rmb = wwRects.bottom - (targetRect.bottom - wsb);
770
- // display margin
771
- 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
+ }
772
826
  }
773
827
 
774
828
  return {
@@ -378,7 +378,7 @@ class Selection_ {
378
378
  /**
379
379
  * @description Scroll to the corresponding selection or range position.
380
380
  * @param {Selection|Range|Node} ref selection or range object
381
- * @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.
382
382
  * @example
383
383
  * // Scroll to current selection smoothly
384
384
  * editor.selection.scrollTo(editor.selection.get());
@@ -394,10 +394,18 @@ class Selection_ {
394
394
  * });
395
395
  */
396
396
  scrollTo(ref, scrollOption) {
397
+ const noFocus = scrollOption?.noFocus;
398
+
397
399
  if (this.#instanceCheck.isSelection(ref)) {
398
400
  ref = ref.getRangeAt(0);
399
401
  } else if (this.#instanceCheck.isNode(ref)) {
400
- 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
+ }
401
409
  } else if (typeof ref?.startContainer === 'undefined') {
402
410
  console.warn('[SUNEDITOR.html.scrollTo.warn] "selectionRange" must be Selection or Range or Node object.', ref);
403
411
  }
@@ -406,6 +414,7 @@ class Selection_ {
406
414
  if (!el) return;
407
415
 
408
416
  scrollOption = { behavior: 'smooth', block: 'nearest', inline: 'nearest', ...scrollOption };
417
+ delete scrollOption.noFocus;
409
418
 
410
419
  const ww = this.#frameContext.get('_ww');
411
420
  const wwFrame = this.#frameContext.get('wysiwygFrame');
@@ -413,6 +422,7 @@ class Selection_ {
413
422
  const isAutoHeight = !this.#store.get('isScrollable')(this.#frameContext);
414
423
  const viewportHeight = this.#store.get('currentViewportHeight');
415
424
  const scrollY = isAutoHeight ? _w.scrollY : isIframe ? ww.scrollY : wwFrame.scrollTop;
425
+ const isBottom = this.#store.mode.isBottom;
416
426
  const realToolbarHeight = this.#context.get('toolbar_main').offsetHeight;
417
427
  const toolbarHeight = this.#$.toolbar.isSticky ? realToolbarHeight : 0;
418
428
  const positionToolbarHeight = this.#$.toolbar.isSticky ? toolbarHeight + this.#options.get('toolbar_sticky') : toolbarHeight;
@@ -422,8 +432,18 @@ class Selection_ {
422
432
  el?.scrollIntoView(scrollOption);
423
433
 
424
434
  if (scrollOption?.behavior === 'auto' && scrollY !== _w.scrollY) {
425
- if (positionToolbarHeight && scrollY > _w.scrollY) {
426
- _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
+ }
427
447
  } else if (isAutoHeight) {
428
448
  _w.scrollBy(0, statusbarHeight);
429
449
  }
@@ -434,55 +454,62 @@ class Selection_ {
434
454
 
435
455
  // --- When there is no upper scroll and it is an iframe ---
436
456
  const PADDING = this.#scrollMargin;
437
- const viewHeight = isAutoHeight ? viewportHeight : wwFrame.offsetHeight;
438
- 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;
439
463
 
440
464
  const behavior = scrollOption?.behavior;
465
+ const topToolbarH = isBottom ? 0 : positionToolbarHeight;
441
466
  if (isAutoHeight) {
442
467
  if (isIframe) {
443
468
  const rect = this.getRects(ref, 'end').rects;
444
- const topMargin = rect.top + elH - positionToolbarHeight;
469
+ const topMargin = rect.top + elH - topToolbarH;
445
470
  const bottomMargin = viewHeight - PADDING - (rect.top + elH);
446
471
  if (topMargin >= 0 && bottomMargin >= 0) return;
447
472
 
448
473
  const newScrollTop = scrollY - (topMargin < 0 ? -(topMargin - PADDING) : bottomMargin);
449
474
  _w.scrollTo({
450
- top: newScrollTop < scrollY ? newScrollTop - positionToolbarHeight : newScrollTop,
475
+ top: newScrollTop < scrollY ? newScrollTop - topToolbarH : newScrollTop,
451
476
  behavior,
452
477
  });
453
478
  } else {
454
479
  const rect = this.#$.offset.getGlobal(el);
455
480
  const scrollMargin = viewHeight + scrollY - rect.top - elH;
456
481
 
457
- if (scrollMargin - PADDING > 0 && viewHeight > scrollMargin + PADDING + positionToolbarHeight) return;
482
+ if (scrollMargin - PADDING > 0 && viewHeight > scrollMargin + PADDING + topToolbarH) return;
458
483
 
459
484
  const newScrollTop = scrollMargin <= PADDING ? scrollY - scrollMargin + PADDING + statusbarHeight : scrollY - scrollMargin + (viewHeight - elH - PADDING);
460
485
  _w.scrollTo({
461
- top: newScrollTop < scrollY ? newScrollTop - positionToolbarHeight : newScrollTop,
486
+ top: newScrollTop < scrollY ? newScrollTop - topToolbarH : newScrollTop,
462
487
  behavior,
463
488
  });
464
489
  }
465
490
  } else {
466
- // local scroll
467
- const { rects } = this.getRects(el, 'start');
468
- const { top } = this.#$.offset.getLocal(el);
469
- 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;
470
495
 
471
496
  const keepLocalScroll = innerTop - PADDING > 0 && innerTop + PADDING <= viewHeight;
472
- 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);
473
498
  let newScrollTop = scrollY + rectScroll;
474
499
 
475
500
  // frame scroll
476
501
  const gy = _w.scrollY;
477
502
  const globalRect = this.#$.offset.getGlobal();
478
- const topMargin = gy - globalRect.top + realToolbarHeight;
479
- 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;
480
507
 
481
508
  // set frame scroll
482
509
  if (topMargin > 0) {
483
510
  const newFrameY = (keepLocalScroll ? innerTop : innerTop + scrollY - newScrollTop) - elH - PADDING - topMargin;
484
511
  if (newFrameY < 0) {
485
- newScrollTop += realToolbarHeight;
512
+ newScrollTop += topToolbarFrame;
486
513
  _w.scrollTo({
487
514
  top: gy + newFrameY,
488
515
  behavior: 'smooth',
@@ -492,7 +519,7 @@ class Selection_ {
492
519
  if (bottomMargin > 0) {
493
520
  const newFrameY = (keepLocalScroll ? innerTop : innerTop + scrollY - newScrollTop) + elH + PADDING - (globalRect.height - bottomMargin);
494
521
  if (newFrameY > 0) {
495
- newScrollTop += statusbarHeight;
522
+ newScrollTop += isBottom ? bottomToolbarFrame : statusbarHeight;
496
523
  _w.scrollTo({
497
524
  top: gy + newFrameY,
498
525
  behavior: 'smooth',