position.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals window */
  6. /**
  7. * @module utils/dom/position
  8. */
  9. import Rect from './rect.js';
  10. import getPositionedAncestor from './getpositionedancestor.js';
  11. /**
  12. * Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the
  13. * target in the visually most efficient way, taking various restrictions like viewport or limiter geometry
  14. * into consideration.
  15. *
  16. * TODO: more docs and example.
  17. *
  18. * @param {module:utils/dom/position~Options} options Positioning options object.
  19. * @returns {Object} An object containing CSS `top`, `left` coordinates ready to use with `position: absolute` and the `name`
  20. * of the position.
  21. */
  22. export function getOptimalPosition( { element, target, positions, limiter, fitInViewport } ) {
  23. const positionedElementAncestor = getPositionedAncestor( element.parentElement );
  24. const elementRect = new Rect( element );
  25. const targetRect = new Rect( target );
  26. let bestPosition;
  27. let name;
  28. // If there are no limits, just grab the very first position and be done with that drama.
  29. if ( !limiter && !fitInViewport ) {
  30. [ name, bestPosition ] = getPosition( positions[ 0 ], targetRect, elementRect );
  31. } else {
  32. const limiterRect = limiter && new Rect( limiter );
  33. const viewportRect = fitInViewport && Rect.getViewportRect();
  34. [ name, bestPosition ] =
  35. getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) ||
  36. // If there's no best position found, i.e. when all intersections have no area because
  37. // rects have no width or height, then just use the first available position.
  38. getPosition( positions[ 0 ], targetRect, elementRect );
  39. }
  40. let { left, top } = getAbsoluteRectCoordinates( bestPosition );
  41. // (#126) If there's some positioned ancestor of the panel, then its rect must be taken into
  42. // consideration. `Rect` is always relative to the viewport while `position: absolute` works
  43. // with respect to that positioned ancestor.
  44. if ( positionedElementAncestor ) {
  45. const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) );
  46. left -= ancestorPosition.left;
  47. top -= ancestorPosition.top;
  48. }
  49. return { left, top, name };
  50. }
  51. // For given position function, returns a corresponding `Rect` instance.
  52. //
  53. // @private
  54. // @param {Function} position A function returning {@link module:utils/dom/position~Position}.
  55. // @param {utils/dom/rect~Rect} targetRect A rect of the target.
  56. // @param {utils/dom/rect~Rect} elementRect A rect of positioned element.
  57. // @returns {Array} An array containing position name and its Rect.
  58. function getPosition( position, targetRect, elementRect ) {
  59. const { left, top, name } = position( targetRect, elementRect );
  60. return [ name, elementRect.clone().moveTo( left, top ) ];
  61. }
  62. // For a given array of positioning functions, returns such that provides the best
  63. // fit of the `elementRect` into the `limiterRect` and `viewportRect`.
  64. //
  65. // @private
  66. // @param {module:utils/dom/position~Options#positions} positions Functions returning
  67. // {@link module:utils/dom/position~Position} to be checked, in the order of preference.
  68. // @param {utils/dom/rect~Rect} targetRect A rect of the {@link module:utils/dom/position~Options#target}.
  69. // @param {utils/dom/rect~Rect} elementRect A rect of positioned {@link module:utils/dom/position~Options#element}.
  70. // @param {utils/dom/rect~Rect} limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}.
  71. // @param {utils/dom/rect~Rect} viewportRect A rect of the viewport.
  72. // @returns {Array} An array containing the name of the position and it's rect.
  73. function getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) {
  74. let maxLimiterIntersectArea = -1;
  75. let maxViewportIntersectArea = -1;
  76. let bestPositionRect;
  77. let bestPositionName;
  78. // This is when element is fully visible.
  79. const elementRectArea = elementRect.getArea();
  80. positions.some( position => {
  81. const [ positionName, positionRect ] = getPosition( position, targetRect, elementRect );
  82. let limiterIntersectArea;
  83. let viewportIntersectArea;
  84. if ( limiterRect ) {
  85. if ( viewportRect ) {
  86. limiterIntersectArea = limiterRect.getIntersection( viewportRect ).getIntersectionArea( positionRect );
  87. } else {
  88. limiterIntersectArea = limiterRect.getIntersectionArea( positionRect );
  89. }
  90. }
  91. if ( viewportRect ) {
  92. viewportIntersectArea = viewportRect.getIntersectionArea( positionRect );
  93. }
  94. // The only criterion: intersection with the viewport.
  95. if ( viewportRect && !limiterRect ) {
  96. if ( viewportIntersectArea > maxViewportIntersectArea ) {
  97. setBestPosition();
  98. }
  99. }
  100. // The only criterion: intersection with the limiter.
  101. else if ( !viewportRect && limiterRect ) {
  102. if ( limiterIntersectArea > maxLimiterIntersectArea ) {
  103. setBestPosition();
  104. }
  105. }
  106. // Two criteria: intersection with the viewport and the limiter visible in the viewport.
  107. else {
  108. if ( viewportIntersectArea > maxViewportIntersectArea && limiterIntersectArea >= maxLimiterIntersectArea ) {
  109. setBestPosition();
  110. } else if ( viewportIntersectArea >= maxViewportIntersectArea && limiterIntersectArea > maxLimiterIntersectArea ) {
  111. setBestPosition();
  112. }
  113. }
  114. function setBestPosition() {
  115. maxViewportIntersectArea = viewportIntersectArea;
  116. maxLimiterIntersectArea = limiterIntersectArea;
  117. bestPositionRect = positionRect;
  118. bestPositionName = positionName;
  119. }
  120. // If a such position is found that element is fully container by the limiter then, obviously,
  121. // there will be no better one, so finishing.
  122. return limiterIntersectArea === elementRectArea;
  123. } );
  124. return bestPositionRect ? [ bestPositionName, bestPositionRect ] : null;
  125. }
  126. // DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't.
  127. // This function converts Rect to `position: absolute` coordinates.
  128. //
  129. // @private
  130. // @param {utils/dom/rect~Rect} rect A rect to be converted.
  131. // @returns {Object} Object containing `left` and `top` properties, in absolute coordinates.
  132. function getAbsoluteRectCoordinates( { left, top } ) {
  133. return {
  134. left: left + window.scrollX,
  135. top: top + window.scrollY,
  136. };
  137. }
  138. /**
  139. * The `getOptimalPosition` helper options.
  140. *
  141. * @interface module:utils/dom/position~Options
  142. */
  143. /**
  144. * Element that is to be positioned.
  145. *
  146. * @member {HTMLElement} module:utils/dom/position~Options#element
  147. */
  148. /**
  149. * Target with respect to which the `element` is to be positioned.
  150. *
  151. * @member {HTMLElement|Range} module:utils/dom/position~Options#target
  152. */
  153. /**
  154. * An array of functions which return {@link module:utils/dom/position~Position} relative
  155. * to the `target`, in the order of preference.
  156. *
  157. * @member {Array.<Function>} module:utils/dom/position~Options#positions
  158. */
  159. /**
  160. * When set, the algorithm will chose position which fits the most in the
  161. * limiter's bounding rect.
  162. *
  163. * @member {HTMLElement|Range} module:utils/dom/position~Options#limiter
  164. */
  165. /**
  166. * When set, the algorithm will chose such a position which fits `element`
  167. * the most inside visible viewport.
  168. *
  169. * @member {Boolean} module:utils/dom/position~Options#fitInViewport
  170. */
  171. /**
  172. * An object describing the position in `position: absolute` coordinate
  173. * system.
  174. *
  175. * @typedef {Object} module:utils/dom/position~Position
  176. *
  177. * @property {Number} top Top position offset.
  178. * @property {Number} left Left position offset.
  179. * @property {String} name Name of the position.
  180. */