position.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module utils/dom/position
  7. */
  8. import global from './global';
  9. import Rect from './rect';
  10. import getPositionedAncestor from './getpositionedancestor';
  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. * // The element which is to be positioned.
  17. * const element = document.body.querySelector( '#toolbar' );
  18. *
  19. * // A target to which the element is positioned relatively.
  20. * const target = document.body.querySelector( '#container' );
  21. *
  22. * // Finding the optimal coordinates for the positioning.
  23. * const { left, top, name } = getOptimalPosition( {
  24. * element: element,
  25. * target: target,
  26. *
  27. * // The algorithm will chose among these positions to meet the requirements such
  28. * // as "limiter" element or "fitInViewport", set below. The positions are considered
  29. * // in the order of the array.
  30. * positions: [
  31. * //
  32. * // [ Target ]
  33. * // +-----------------+
  34. * // | Element |
  35. * // +-----------------+
  36. * //
  37. * targetRect => ( {
  38. * top: targetRect.bottom,
  39. * left: targetRect.left,
  40. * name: 'mySouthEastPosition'
  41. * } ),
  42. *
  43. * //
  44. * // +-----------------+
  45. * // | Element |
  46. * // +-----------------+
  47. * // [ Target ]
  48. * //
  49. * ( targetRect, elementRect ) => ( {
  50. * top: targetRect.top - elementRect.height,
  51. * left: targetRect.left,
  52. * name: 'myNorthEastPosition'
  53. * } )
  54. * ],
  55. *
  56. * // Find a position such guarantees the element remains within visible boundaries of <body>.
  57. * limiter: document.body,
  58. *
  59. * // Find a position such guarantees the element remains within visible boundaries of the browser viewport.
  60. * fitInViewport: true
  61. * } );
  62. *
  63. * // The best position which fits into document.body and the viewport. May be useful
  64. * // to set proper class on the `element`.
  65. * console.log( name ); -> "myNorthEastPosition"
  66. *
  67. * // Using the absolute coordinates which has been found to position the element
  68. * // as in the diagram depicting the "myNorthEastPosition" position.
  69. * element.style.top = top;
  70. * element.style.left = left;
  71. *
  72. * @param {module:utils/dom/position~Options} options Positioning options object.
  73. * @returns {module:utils/dom/position~Position}
  74. */
  75. export function getOptimalPosition( { element, target, positions, limiter, fitInViewport } ) {
  76. const positionedElementAncestor = getPositionedAncestor( element.parentElement );
  77. const elementRect = new Rect( element );
  78. const targetRect = new Rect( target );
  79. let bestPosition;
  80. let name;
  81. // If there are no limits, just grab the very first position and be done with that drama.
  82. if ( !limiter && !fitInViewport ) {
  83. [ name, bestPosition ] = getPosition( positions[ 0 ], targetRect, elementRect );
  84. } else {
  85. const limiterRect = limiter && new Rect( limiter );
  86. const viewportRect = fitInViewport && Rect.getViewportRect();
  87. [ name, bestPosition ] =
  88. getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) ||
  89. // If there's no best position found, i.e. when all intersections have no area because
  90. // rects have no width or height, then just use the first available position.
  91. getPosition( positions[ 0 ], targetRect, elementRect );
  92. }
  93. let { left, top } = getAbsoluteRectCoordinates( bestPosition );
  94. // (#126) If there's some positioned ancestor of the panel, then its rect must be taken into
  95. // consideration. `Rect` is always relative to the viewport while `position: absolute` works
  96. // with respect to that positioned ancestor.
  97. if ( positionedElementAncestor ) {
  98. const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) );
  99. left -= ancestorPosition.left;
  100. top -= ancestorPosition.top;
  101. }
  102. return { left, top, name };
  103. }
  104. // For given position function, returns a corresponding `Rect` instance.
  105. //
  106. // @private
  107. // @param {Function} position A function returning {@link module:utils/dom/position~Position}.
  108. // @param {utils/dom/rect~Rect} targetRect A rect of the target.
  109. // @param {utils/dom/rect~Rect} elementRect A rect of positioned element.
  110. // @returns {Array} An array containing position name and its Rect.
  111. function getPosition( position, targetRect, elementRect ) {
  112. const { left, top, name } = position( targetRect, elementRect );
  113. return [ name, elementRect.clone().moveTo( left, top ) ];
  114. }
  115. // For a given array of positioning functions, returns such that provides the best
  116. // fit of the `elementRect` into the `limiterRect` and `viewportRect`.
  117. //
  118. // @private
  119. // @param {module:utils/dom/position~Options#positions} positions Functions returning
  120. // {@link module:utils/dom/position~Position} to be checked, in the order of preference.
  121. // @param {utils/dom/rect~Rect} targetRect A rect of the {@link module:utils/dom/position~Options#target}.
  122. // @param {utils/dom/rect~Rect} elementRect A rect of positioned {@link module:utils/dom/position~Options#element}.
  123. // @param {utils/dom/rect~Rect} limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}.
  124. // @param {utils/dom/rect~Rect} viewportRect A rect of the viewport.
  125. // @returns {Array} An array containing the name of the position and it's rect.
  126. function getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) {
  127. let maxLimiterIntersectArea = 0;
  128. let maxViewportIntersectArea = 0;
  129. let bestPositionRect;
  130. let bestPositionName;
  131. // This is when element is fully visible.
  132. const elementRectArea = elementRect.getArea();
  133. positions.some( position => {
  134. const [ positionName, positionRect ] = getPosition( position, targetRect, elementRect );
  135. let limiterIntersectArea;
  136. let viewportIntersectArea;
  137. if ( limiterRect ) {
  138. if ( viewportRect ) {
  139. // Consider only the part of the limiter which is visible in the viewport. So the limiter is getting limited.
  140. const limiterViewportIntersectRect = limiterRect.getIntersection( viewportRect );
  141. if ( limiterViewportIntersectRect ) {
  142. // If the limiter is within the viewport, then check the intersection between that part of the
  143. // limiter and actual position.
  144. limiterIntersectArea = limiterViewportIntersectRect.getIntersectionArea( positionRect );
  145. } else {
  146. limiterIntersectArea = 0;
  147. }
  148. } else {
  149. limiterIntersectArea = limiterRect.getIntersectionArea( positionRect );
  150. }
  151. }
  152. if ( viewportRect ) {
  153. viewportIntersectArea = viewportRect.getIntersectionArea( positionRect );
  154. }
  155. // The only criterion: intersection with the viewport.
  156. if ( viewportRect && !limiterRect ) {
  157. if ( viewportIntersectArea > maxViewportIntersectArea ) {
  158. setBestPosition();
  159. }
  160. }
  161. // The only criterion: intersection with the limiter.
  162. else if ( !viewportRect && limiterRect ) {
  163. if ( limiterIntersectArea > maxLimiterIntersectArea ) {
  164. setBestPosition();
  165. }
  166. }
  167. // Two criteria: intersection with the viewport and the limiter visible in the viewport.
  168. else {
  169. if ( viewportIntersectArea > maxViewportIntersectArea && limiterIntersectArea >= maxLimiterIntersectArea ) {
  170. setBestPosition();
  171. } else if ( viewportIntersectArea >= maxViewportIntersectArea && limiterIntersectArea > maxLimiterIntersectArea ) {
  172. setBestPosition();
  173. }
  174. }
  175. function setBestPosition() {
  176. maxViewportIntersectArea = viewportIntersectArea;
  177. maxLimiterIntersectArea = limiterIntersectArea;
  178. bestPositionRect = positionRect;
  179. bestPositionName = positionName;
  180. }
  181. // If a such position is found that element is fully container by the limiter then, obviously,
  182. // there will be no better one, so finishing.
  183. return limiterIntersectArea === elementRectArea;
  184. } );
  185. return bestPositionRect ? [ bestPositionName, bestPositionRect ] : null;
  186. }
  187. // DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't.
  188. // This function converts Rect to `position: absolute` coordinates.
  189. //
  190. // @private
  191. // @param {utils/dom/rect~Rect} rect A rect to be converted.
  192. // @returns {Object} Object containing `left` and `top` properties, in absolute coordinates.
  193. function getAbsoluteRectCoordinates( { left, top } ) {
  194. const { scrollX, scrollY } = global.window;
  195. return {
  196. left: left + scrollX,
  197. top: top + scrollY,
  198. };
  199. }
  200. /**
  201. * The `getOptimalPosition` helper options.
  202. *
  203. * @interface module:utils/dom/position~Options
  204. */
  205. /**
  206. * Element that is to be positioned.
  207. *
  208. * @member {HTMLElement} #element
  209. */
  210. /**
  211. * Target with respect to which the `element` is to be positioned.
  212. *
  213. * @member {HTMLElement|Range|ClientRect} #target
  214. */
  215. /**
  216. * An array of functions which return {@link module:utils/dom/position~Position} relative
  217. * to the `target`, in the order of preference.
  218. *
  219. * @member {Array.<Function>} #positions
  220. */
  221. /**
  222. * When set, the algorithm will chose position which fits the most in the
  223. * limiter's bounding rect.
  224. *
  225. * @member {HTMLElement|Range|ClientRect} #limiter
  226. */
  227. /**
  228. * When set, the algorithm will chose such a position which fits `element`
  229. * the most inside visible viewport.
  230. *
  231. * @member {Boolean} #fitInViewport
  232. */
  233. /**
  234. * An object describing a position in `position: absolute` coordinate
  235. * system, along with position name.
  236. *
  237. * @typedef {Object} module:utils/dom/position~Position
  238. *
  239. * @property {Number} top Top position offset.
  240. * @property {Number} left Left position offset.
  241. * @property {String} name Name of the position.
  242. */