/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module utils/dom/position
*/
import global from './global';
import Rect from './rect';
import getPositionedAncestor from './getpositionedancestor';
import getBorderWidths from './getborderwidths';
import { isFunction } from 'lodash-es';
/**
* 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.
*
* // The element which is to be positioned.
* const element = document.body.querySelector( '#toolbar' );
*
* // A target to which the element is positioned relatively.
* const target = document.body.querySelector( '#container' );
*
* // Finding the optimal coordinates for the positioning.
* const { left, top, name } = getOptimalPosition( {
* element: element,
* target: target,
*
* // The algorithm will chose among these positions to meet the requirements such
* // as "limiter" element or "fitInViewport", set below. The positions are considered
* // in the order of the array.
* positions: [
* //
* // [ Target ]
* // +-----------------+
* // | Element |
* // +-----------------+
* //
* targetRect => ( {
* top: targetRect.bottom,
* left: targetRect.left,
* name: 'mySouthEastPosition'
* } ),
*
* //
* // +-----------------+
* // | Element |
* // +-----------------+
* // [ Target ]
* //
* ( targetRect, elementRect ) => ( {
* top: targetRect.top - elementRect.height,
* left: targetRect.left,
* name: 'myNorthEastPosition'
* } )
* ],
*
* // Find a position such guarantees the element remains within visible boundaries of
.
* limiter: document.body,
*
* // Find a position such guarantees the element remains within visible boundaries of the browser viewport.
* fitInViewport: true
* } );
*
* // The best position which fits into document.body and the viewport. May be useful
* // to set proper class on the `element`.
* console.log( name ); // -> "myNorthEastPosition"
*
* // Using the absolute coordinates which has been found to position the element
* // as in the diagram depicting the "myNorthEastPosition" position.
* element.style.top = top;
* element.style.left = left;
*
* @param {module:utils/dom/position~Options} options Positioning options object.
* @returns {module:utils/dom/position~Position}
*/
export function getOptimalPosition( { element, target, positions, limiter, fitInViewport } ) {
// If the {@link module:utils/dom/position~Options#target} is a function, use what it returns.
// https://github.com/ckeditor/ckeditor5-utils/issues/157
if ( isFunction( target ) ) {
target = target();
}
// If the {@link module:utils/dom/position~Options#limiter} is a function, use what it returns.
// https://github.com/ckeditor/ckeditor5-ui/issues/260
if ( isFunction( limiter ) ) {
limiter = limiter();
}
const positionedElementAncestor = getPositionedAncestor( element );
const elementRect = new Rect( element );
const targetRect = new Rect( target );
let bestPositionRect;
let bestPositionName;
// If there are no limits, just grab the very first position and be done with that drama.
if ( !limiter && !fitInViewport ) {
[ bestPositionName, bestPositionRect ] = getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
} else {
const limiterRect = limiter && new Rect( limiter ).getVisible();
const viewportRect = fitInViewport && new Rect( global.window );
const bestPosition = getBestPositionNameAndRect( 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.
[ bestPositionName, bestPositionRect ] = bestPosition || getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
}
let absoluteRectCoordinates = getAbsoluteRectCoordinates( bestPositionRect );
if ( positionedElementAncestor ) {
absoluteRectCoordinates = shiftRectCoordinatesDueToPositionedAncestor( absoluteRectCoordinates, positionedElementAncestor );
}
return {
left: absoluteRectCoordinates.left,
top: absoluteRectCoordinates.top,
name: bestPositionName
};
}
// 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|null} An array containing position name and its Rect (or null if position should be ignored).
function getPositionNameAndRect( position, targetRect, elementRect ) {
const positionData = position( targetRect, elementRect );
if ( !positionData ) {
return null;
}
const { left, top, name } = positionData;
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 {Object} options
// @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 {Object} options
// @param {utils/dom/rect~Rect} options.targetRect A rect of the {@link module:utils/dom/position~Options#target}.
// @param {utils/dom/rect~Rect} options.elementRect A rect of positioned {@link module:utils/dom/position~Options#element}.
// @param {utils/dom/rect~Rect} options.limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}.
// @param {utils/dom/rect~Rect} options.viewportRect A rect of the viewport.
//
// @returns {Array} An array containing the name of the position and it's rect.
function getBestPositionNameAndRect( positions, options ) {
const { elementRect, viewportRect } = options;
// This is when element is fully visible.
const elementRectArea = elementRect.getArea();
// Let's calculate intersection areas for positions. It will end early if best match is found.
const processedPositions = processPositionsToAreas( positions, options );
// First let's check all positions that fully fit in the viewport.
if ( viewportRect ) {
const processedPositionsInViewport = processedPositions.filter( ( { viewportIntersectArea } ) => {
return viewportIntersectArea === elementRectArea;
} );
// Try to find best position from those which fit completely in viewport.
const bestPositionData = getBestOfProcessedPositions( processedPositionsInViewport, elementRectArea );
if ( bestPositionData ) {
return bestPositionData;
}
}
// Either there is no viewportRect or there is no position that fits completely in the viewport.
return getBestOfProcessedPositions( processedPositions, elementRectArea );
}
// For a given array of positioning functions, calculates intersection areas for them.
//
// Note: If some position fully fits into the `limiterRect`, it will be returned early, without further consideration
// of other positions.
//
// @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 {Object} options
// @param {utils/dom/rect~Rect} options.targetRect A rect of the {@link module:utils/dom/position~Options#target}.
// @param {utils/dom/rect~Rect} options.elementRect A rect of positioned {@link module:utils/dom/position~Options#element}.
// @param {utils/dom/rect~Rect} options.limiterRect A rect of the {@link module:utils/dom/position~Options#limiter}.
// @param {utils/dom/rect~Rect} options.viewportRect A rect of the viewport.
//
// @returns {Array.