8
0

scroll.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global Node */
  6. /**
  7. * @module utils/dom/scroll
  8. */
  9. import global from './global';
  10. import isRange from './isrange';
  11. import Rect from './rect';
  12. /**
  13. * Makes any page `HTMLElement` or `Range` (`target`) visible inside the browser viewport.
  14. * This helper will scroll all `target` ancestors and the web browser viewport to reveal the target to
  15. * the user. If the `target` is already visible, nothing will happen.
  16. *
  17. * @param {HTMLElement|Range} options.target A target, which supposed to become visible to the user.
  18. * @param {Number} [options.viewportOffset] An offset from the edge of the viewport (in pixels)
  19. * the `target` will be moved by when the viewport is scrolled. It enhances the user experience
  20. * by keeping the `target` some distance from the edge of the viewport and thus making it easier to
  21. * read or edit by the user.
  22. */
  23. function scrollViewportToShowTarget( { target, viewportOffset = 0 } ) {
  24. // Scroll the ancestors of the target to reveal it first, then focus on scrolling
  25. // the viewport, when the position of the target is fixed.
  26. scrollAncestorsToShowTarget( target );
  27. const targetRect = new Rect( target );
  28. const targetShiftedDownRect = targetRect.clone().moveBy( 0, viewportOffset );
  29. const targetShiftedUpRect = targetRect.clone().moveBy( 0, -viewportOffset );
  30. const viewportRect = new Rect( global.window ).excludeScrollbarsAndBorders();
  31. // Avoid the situation where the caret is still in the viewport, but totally
  32. // at the edge. If it moved beyond the viewport in the next action e.g. due to enter,
  33. // the scrolling would move it to the viewportOffset level and it all would look like the
  34. // caret visually moved up/down.
  35. //
  36. // To prevent this, we're checking the rects moved by the viewportOffset to cover
  37. // the upper/lower edge of the viewport.
  38. const rects = [ targetShiftedUpRect, targetShiftedDownRect ];
  39. if ( !rects.every( rect => viewportRect.contains( rect ) ) ) {
  40. let { scrollX, scrollY } = global.window;
  41. if ( isAbove( targetShiftedUpRect, viewportRect ) ) {
  42. scrollY -= viewportRect.top - targetRect.top + viewportOffset;
  43. } else if ( isBelow( targetShiftedDownRect, viewportRect ) ) {
  44. scrollY += targetRect.bottom - viewportRect.bottom + viewportOffset;
  45. }
  46. // TODO: Web browsers scroll natively to place the target in the middle
  47. // of the viewport. It's not a very popular case, though.
  48. if ( isLeftOf( targetRect, viewportRect ) ) {
  49. scrollX -= viewportRect.left - targetRect.left + viewportOffset;
  50. } else if ( isRightOf( targetRect, viewportRect ) ) {
  51. scrollX += targetRect.right - viewportRect.right + viewportOffset;
  52. }
  53. global.window.scrollTo( scrollX, scrollY );
  54. }
  55. }
  56. /**
  57. * Makes any page `HTMLElement` or `Range` (target) visible within its scrollable ancestors,
  58. * e.g. if they have `overflow: scroll` CSS style.
  59. *
  60. * @param {HTMLElement|Range} target A target, which supposed to become visible to the user.
  61. */
  62. function scrollAncestorsToShowTarget( target ) {
  63. let parent, parentRect, targetRect;
  64. if ( isRange( target ) ) {
  65. parent = target.commonAncestorContainer;
  66. // If a Range is attached to the Text, use the closest element ancestor.
  67. if ( parent.nodeType == Node.TEXT_NODE ) {
  68. parent = parent.parentNode;
  69. }
  70. } else {
  71. parent = target.parentNode;
  72. }
  73. do {
  74. if ( parent === global.document.body ) {
  75. return;
  76. }
  77. targetRect = new Rect( target );
  78. parentRect = new Rect( parent ).excludeScrollbarsAndBorders();
  79. if ( !parentRect.contains( targetRect ) ) {
  80. scrollAncestorToShowTarget( { targetRect, parent, parentRect } );
  81. }
  82. } while ( ( parent = parent.parentNode ) );
  83. }
  84. export default {
  85. scrollViewportToShowTarget,
  86. scrollAncestorsToShowTarget
  87. };
  88. // Makes any page `HTMLElement` or `Range` (target) visible within its parent.
  89. //
  90. // @private
  91. // @param {module:utils/dom/rect~Rect} options.targetRect The `Rect` of the `target`.
  92. // @param {HTMLElement} options.parent The parent element of the `target`.
  93. // @param {module:utils/dom/rect~Rect} options.parentRect The `Rect` of the parent.
  94. function scrollAncestorToShowTarget( { targetRect, parent, parentRect } ) {
  95. if ( isAbove( targetRect, parentRect ) ) {
  96. parent.scrollTop -= parentRect.top - targetRect.top;
  97. } else if ( isBelow( targetRect, parentRect ) ) {
  98. parent.scrollTop += targetRect.bottom - parentRect.bottom;
  99. }
  100. if ( isLeftOf( targetRect, parentRect ) ) {
  101. parent.scrollLeft -= parentRect.left - targetRect.left;
  102. } else if ( isRightOf( targetRect, parentRect ) ) {
  103. parent.scrollLeft += targetRect.right - parentRect.right;
  104. }
  105. }
  106. // Determines if a given `Rect` extends beyond the bottom edge of the second `Rect`.
  107. //
  108. // @private
  109. // @param {module:utils/dom/rect~Rect} firstRect
  110. // @param {module:utils/dom/rect~Rect} secondRect
  111. function isBelow( firstRect, secondRect ) {
  112. return firstRect.bottom > secondRect.bottom;
  113. }
  114. // Determines if a given `Rect` extends beyond the top edge of the second `Rect`.
  115. //
  116. // @private
  117. // @param {module:utils/dom/rect~Rect} firstRect
  118. // @param {module:utils/dom/rect~Rect} secondRect
  119. function isAbove( firstRect, secondRect ) {
  120. return firstRect.top < secondRect.top;
  121. }
  122. // Determines if a given `Rect` extends beyond the left edge of the second `Rect`.
  123. //
  124. // @private
  125. // @param {module:utils/dom/rect~Rect} firstRect
  126. // @param {module:utils/dom/rect~Rect} secondRect
  127. function isLeftOf( firstRect, secondRect ) {
  128. return firstRect.left < secondRect.left;
  129. }
  130. // Determines if a given `Rect` extends beyond the right edge of the second `Rect`.
  131. //
  132. // @private
  133. // @param {module:utils/dom/rect~Rect} firstRect
  134. // @param {module:utils/dom/rect~Rect} secondRect
  135. function isRightOf( firstRect, secondRect ) {
  136. return firstRect.right > secondRect.right;
  137. }