scroll.js 6.0 KB

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