/** * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ /* globals window */ /** * @module utils/dom/position */ import Rect from './rect.js'; import getPositionedAncestor from './getpositionedancestor.js'; /** * Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the * target in the visually most efficient way, taking various restrictions like viewport or limiter geometry * into consideration. * * TODO: more docs and example. * * @param {module:utils/dom/position~Options} options Positioning options object. * @returns {Object} An object containing CSS `top`, `left` coordinates ready to use with `position: absolute` and the `name` * of the position. */ export function getOptimalPosition( { element, target, positions, limiter, fitInViewport } ) { const positionedElementAncestor = getPositionedAncestor( element.parentElement ); const elementRect = new Rect( element ); const targetRect = new Rect( target ); let bestPosition; let name; // If there are no limits, just grab the very first position and be done with that drama. if ( !limiter && !fitInViewport ) { [ name, bestPosition ] = getPosition( positions[ 0 ], targetRect, elementRect ); } else { const limiterRect = limiter && new Rect( limiter ); const viewportRect = fitInViewport && Rect.getViewportRect(); [ name, bestPosition ] = getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) || // If there's no best position found, i.e. when all intersections have no area because // rects have no width or height, then just use the first available position. getPosition( positions[ 0 ], targetRect, elementRect ); } let { left, top } = getAbsoluteRectCoordinates( bestPosition ); // (#126) If there's some positioned ancestor of the panel, then its rect must be taken into // consideration. `Rect` is always relative to the viewport while `position: absolute` works // with respect to that positioned ancestor. if ( positionedElementAncestor ) { const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) ); left -= ancestorPosition.left; top -= ancestorPosition.top; } return { left, top, name }; } // For given position function, returns a corresponding `Rect` instance. // // @private // @param {Function} position A function returning {@link module:utils/dom/position~Position}. // @param {utils/dom/rect~Rect} targetRect A rect of the target. // @param {utils/dom/rect~Rect} elementRect A rect of positioned element. // @returns {Array} An array containing position name and its Rect. function getPosition( position, targetRect, elementRect ) { const { left, top, name } = position( targetRect, elementRect ); return [ name, elementRect.clone().moveTo( left, top ) ]; } // For a given array of positioning functions, returns such that provides the best // fit of the `elementRect` into the `limiterRect` and `viewportRect`. // // @private // @param {module:utils/dom/position~Options#positions} positions Functions returning // {@link module:utils/dom/position~Position} to be checked, in the order of preference. // @param {utils/dom/rect~Rect} targetRect A rect of the {@link module:utils/dom/position~Options#target}. // @param {utils/dom/rect~Rect} elementRect A rect of positioned {@link module:utils/dom/position~Options#element}. // @param {utils/dom/rect~Rect} limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}. // @param {utils/dom/rect~Rect} viewportRect A rect of the viewport. // @returns {Array} An array containing the name of the position and it's rect. function getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) { let maxLimiterIntersectArea = -1; let maxViewportIntersectArea = -1; let bestPositionRect; let bestPositionName; // This is when element is fully visible. const elementRectArea = elementRect.getArea(); positions.some( position => { const [ positionName, positionRect ] = getPosition( position, targetRect, elementRect ); let limiterIntersectArea; let viewportIntersectArea; if ( limiterRect ) { if ( viewportRect ) { limiterIntersectArea = limiterRect.getIntersection( viewportRect ).getIntersectionArea( positionRect ); } else { limiterIntersectArea = limiterRect.getIntersectionArea( positionRect ); } } if ( viewportRect ) { viewportIntersectArea = viewportRect.getIntersectionArea( positionRect ); } // The only criterion: intersection with the viewport. if ( viewportRect && !limiterRect ) { if ( viewportIntersectArea > maxViewportIntersectArea ) { setBestPosition(); } } // The only criterion: intersection with the limiter. else if ( !viewportRect && limiterRect ) { if ( limiterIntersectArea > maxLimiterIntersectArea ) { setBestPosition(); } } // Two criteria: intersection with the viewport and the limiter visible in the viewport. else { if ( viewportIntersectArea > maxViewportIntersectArea && limiterIntersectArea >= maxLimiterIntersectArea ) { setBestPosition(); } else if ( viewportIntersectArea >= maxViewportIntersectArea && limiterIntersectArea > maxLimiterIntersectArea ) { setBestPosition(); } } function setBestPosition() { maxViewportIntersectArea = viewportIntersectArea; maxLimiterIntersectArea = limiterIntersectArea; bestPositionRect = positionRect; bestPositionName = positionName; } // If a such position is found that element is fully container by the limiter then, obviously, // there will be no better one, so finishing. return limiterIntersectArea === elementRectArea; } ); return bestPositionRect ? [ bestPositionName, bestPositionRect ] : null; } // DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't. // This function converts Rect to `position: absolute` coordinates. // // @private // @param {utils/dom/rect~Rect} rect A rect to be converted. // @returns {Object} Object containing `left` and `top` properties, in absolute coordinates. function getAbsoluteRectCoordinates( { left, top } ) { return { left: left + window.scrollX, top: top + window.scrollY, }; } /** * The `getOptimalPosition` helper options. * * @interface module:utils/dom/position~Options */ /** * Element that is to be positioned. * * @member {HTMLElement} module:utils/dom/position~Options#element */ /** * Target with respect to which the `element` is to be positioned. * * @member {HTMLElement|Range} module:utils/dom/position~Options#target */ /** * An array of functions which return {@link module:utils/dom/position~Position} relative * to the `target`, in the order of preference. * * @member {Array.} module:utils/dom/position~Options#positions */ /** * When set, the algorithm will chose position which fits the most in the * limiter's bounding rect. * * @member {HTMLElement|Range} module:utils/dom/position~Options#limiter */ /** * When set, the algorithm will chose such a position which fits `element` * the most inside visible viewport. * * @member {Boolean} module:utils/dom/position~Options#fitInViewport */ /** * An object describing the position in `position: absolute` coordinate * system. * * @typedef {Object} module:utils/dom/position~Position * * @property {Number} top Top position offset. * @property {Number} left Left position offset. * @property {String} name Name of the position. */