|
@@ -94,54 +94,33 @@ export function getOptimalPosition( { element, target, positions, limiter, fitIn
|
|
|
const elementRect = new Rect( element );
|
|
const elementRect = new Rect( element );
|
|
|
const targetRect = new Rect( target );
|
|
const targetRect = new Rect( target );
|
|
|
|
|
|
|
|
- let bestPosition;
|
|
|
|
|
- let name;
|
|
|
|
|
|
|
+ let bestPositionRect;
|
|
|
|
|
+ let bestPositionName;
|
|
|
|
|
|
|
|
// If there are no limits, just grab the very first position and be done with that drama.
|
|
// If there are no limits, just grab the very first position and be done with that drama.
|
|
|
if ( !limiter && !fitInViewport ) {
|
|
if ( !limiter && !fitInViewport ) {
|
|
|
- [ name, bestPosition ] = getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
|
|
|
|
|
|
|
+ [ bestPositionName, bestPositionRect ] = getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
|
|
|
} else {
|
|
} else {
|
|
|
const limiterRect = limiter && new Rect( limiter ).getVisible();
|
|
const limiterRect = limiter && new Rect( limiter ).getVisible();
|
|
|
const viewportRect = fitInViewport && new Rect( global.window );
|
|
const viewportRect = fitInViewport && new Rect( global.window );
|
|
|
|
|
+ const bestPosition = getBestPositionNameAndRect( positions, { targetRect, elementRect, limiterRect, viewportRect } );
|
|
|
|
|
|
|
|
- [ 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.
|
|
|
|
|
- getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
|
|
|
|
|
|
|
+ // 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 { left, top } = getAbsoluteRectCoordinates( bestPosition );
|
|
|
|
|
|
|
+ let absoluteRectCoordinates = getAbsoluteRectCoordinates( bestPositionRect );
|
|
|
|
|
|
|
|
if ( positionedElementAncestor ) {
|
|
if ( positionedElementAncestor ) {
|
|
|
- const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) );
|
|
|
|
|
- const ancestorBorderWidths = getBorderWidths( positionedElementAncestor );
|
|
|
|
|
-
|
|
|
|
|
- // (https://github.com/ckeditor/ckeditor5-ui-default/issues/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.
|
|
|
|
|
- left -= ancestorPosition.left;
|
|
|
|
|
- top -= ancestorPosition.top;
|
|
|
|
|
-
|
|
|
|
|
- // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
|
|
|
|
|
- // If there's some positioned ancestor of the panel, not only its position must be taken into
|
|
|
|
|
- // consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect`
|
|
|
|
|
- // is relative to the viewport (it doesn't care about scrolling), while `position: absolute`
|
|
|
|
|
- // must compensate that scrolling.
|
|
|
|
|
- left += positionedElementAncestor.scrollLeft;
|
|
|
|
|
- top += positionedElementAncestor.scrollTop;
|
|
|
|
|
-
|
|
|
|
|
- // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
|
|
|
|
|
- // If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth`
|
|
|
|
|
- // while `position: absolute` positioning does not consider it.
|
|
|
|
|
- // E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element,
|
|
|
|
|
- // not upper-left corner of its border.
|
|
|
|
|
- left -= ancestorBorderWidths.left;
|
|
|
|
|
- top -= ancestorBorderWidths.top;
|
|
|
|
|
|
|
+ absoluteRectCoordinates = shiftRectCoordinatesDueToPositionedAncestor( absoluteRectCoordinates, positionedElementAncestor );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return { left, top, name };
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ left: absoluteRectCoordinates.left,
|
|
|
|
|
+ top: absoluteRectCoordinates.top,
|
|
|
|
|
+ name: bestPositionName
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// For given position function, returns a corresponding `Rect` instance.
|
|
// For given position function, returns a corresponding `Rect` instance.
|
|
@@ -161,24 +140,31 @@ function getPositionNameAndRect( position, targetRect, elementRect ) {
|
|
|
// fit of the `elementRect` into the `limiterRect` and `viewportRect`.
|
|
// fit of the `elementRect` into the `limiterRect` and `viewportRect`.
|
|
|
//
|
|
//
|
|
|
// @private
|
|
// @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.
|
|
|
|
|
|
|
+//
|
|
|
|
|
+// @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.
|
|
// @returns {Array} An array containing the name of the position and it's rect.
|
|
|
-function getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) {
|
|
|
|
|
|
|
+function getBestPositionNameAndRect( positions, options ) {
|
|
|
|
|
+ const { elementRect, viewportRect } = options;
|
|
|
|
|
+
|
|
|
// This is when element is fully visible.
|
|
// This is when element is fully visible.
|
|
|
const elementRectArea = elementRect.getArea();
|
|
const elementRectArea = elementRect.getArea();
|
|
|
|
|
|
|
|
// Let's calculate intersection areas for positions. It will end early if best match is found.
|
|
// Let's calculate intersection areas for positions. It will end early if best match is found.
|
|
|
- const processedPositions = processPositionsToAreas( positions, targetRect, elementRect, limiterRect, viewportRect );
|
|
|
|
|
|
|
+ const processedPositions = processPositionsToAreas( positions, options );
|
|
|
|
|
|
|
|
// First let's check all positions that fully fit in the viewport.
|
|
// First let's check all positions that fully fit in the viewport.
|
|
|
if ( viewportRect ) {
|
|
if ( viewportRect ) {
|
|
|
- const processedPositionsInViewport = processedPositions
|
|
|
|
|
- .filter( ( { viewportIntersectArea } ) => viewportIntersectArea === elementRectArea );
|
|
|
|
|
|
|
+ const processedPositionsInViewport = processedPositions.filter( ( { viewportIntersectArea } ) => {
|
|
|
|
|
+ return viewportIntersectArea === elementRectArea;
|
|
|
|
|
+ } );
|
|
|
|
|
|
|
|
// Try to find best position from those which fit completely in viewport.
|
|
// Try to find best position from those which fit completely in viewport.
|
|
|
const bestPositionData = getBestOfProcessedPositions( processedPositionsInViewport, elementRectArea );
|
|
const bestPositionData = getBestOfProcessedPositions( processedPositionsInViewport, elementRectArea );
|
|
@@ -198,18 +184,21 @@ function getBestPosition( positions, targetRect, elementRect, limiterRect, viewp
|
|
|
// of other positions.
|
|
// of other positions.
|
|
|
//
|
|
//
|
|
|
// @private
|
|
// @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.
|
|
|
|
|
|
|
+//
|
|
|
|
|
+// @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.<Object>} Array of positions with calculated intersection areas. Each item is an object containing:
|
|
// @returns {Array.<Object>} Array of positions with calculated intersection areas. Each item is an object containing:
|
|
|
// * {String} positionName Name of position.
|
|
// * {String} positionName Name of position.
|
|
|
// * {utils/dom/rect~Rect} positionRect Rect of position.
|
|
// * {utils/dom/rect~Rect} positionRect Rect of position.
|
|
|
// * {Number} limiterIntersectArea Area of intersection of the position with limiter part that is in the viewport.
|
|
// * {Number} limiterIntersectArea Area of intersection of the position with limiter part that is in the viewport.
|
|
|
// * {Number} viewportIntersectArea Area of intersection of the position with viewport.
|
|
// * {Number} viewportIntersectArea Area of intersection of the position with viewport.
|
|
|
-function processPositionsToAreas( positions, targetRect, elementRect, limiterRect, viewportRect ) {
|
|
|
|
|
|
|
+function processPositionsToAreas( positions, { targetRect, elementRect, limiterRect, viewportRect } ) {
|
|
|
const processedPositions = [];
|
|
const processedPositions = [];
|
|
|
|
|
|
|
|
// This is when element is fully visible.
|
|
// This is when element is fully visible.
|
|
@@ -259,9 +248,9 @@ function processPositionsToAreas( positions, targetRect, elementRect, limiterRec
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// For a given array of processed position data (with calculated Rects for positions and intersection areas)
|
|
// For a given array of processed position data (with calculated Rects for positions and intersection areas)
|
|
|
-// returns such that provides the best fit of the `elementRect` into the `limiterRect` and `viewportRect`.
|
|
|
|
|
|
|
+// returns such that provides the best fit of the `elementRect` into the `limiterRect` and `viewportRect` at the same time.
|
|
|
//
|
|
//
|
|
|
-// It will return early if found a position that fully fits into the `limiterRect`.
|
|
|
|
|
|
|
+// **Note**: It will return early if some position fully fits into the `limiterRect`.
|
|
|
//
|
|
//
|
|
|
// @private
|
|
// @private
|
|
|
// @param {Array.<Object>} Array of positions with calculated intersection areas (in order of preference).
|
|
// @param {Array.<Object>} Array of positions with calculated intersection areas (in order of preference).
|
|
@@ -279,7 +268,13 @@ function getBestOfProcessedPositions( processedPositions, elementRectArea ) {
|
|
|
let bestPositionRect;
|
|
let bestPositionRect;
|
|
|
let bestPositionName;
|
|
let bestPositionName;
|
|
|
|
|
|
|
|
- processedPositions.some( ( { positionName, positionRect, limiterIntersectArea, viewportIntersectArea } ) => {
|
|
|
|
|
|
|
+ for ( const { positionName, positionRect, limiterIntersectArea, viewportIntersectArea } of processedPositions ) {
|
|
|
|
|
+ // If a such position is found that element is fully container by the limiter then, obviously,
|
|
|
|
|
+ // there will be no better one, so finishing.
|
|
|
|
|
+ if ( limiterIntersectArea === elementRectArea ) {
|
|
|
|
|
+ return [ positionName, positionRect ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// To maximize both viewport and limiter intersection areas we use distance on viewportIntersectArea
|
|
// To maximize both viewport and limiter intersection areas we use distance on viewportIntersectArea
|
|
|
// and limiterIntersectArea plane (without sqrt because we are looking for max value).
|
|
// and limiterIntersectArea plane (without sqrt because we are looking for max value).
|
|
|
const fitFactor = viewportIntersectArea ** 2 + limiterIntersectArea ** 2;
|
|
const fitFactor = viewportIntersectArea ** 2 + limiterIntersectArea ** 2;
|
|
@@ -289,15 +284,57 @@ function getBestOfProcessedPositions( processedPositions, elementRectArea ) {
|
|
|
bestPositionRect = positionRect;
|
|
bestPositionRect = positionRect;
|
|
|
bestPositionName = positionName;
|
|
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;
|
|
return bestPositionRect ? [ bestPositionName, bestPositionRect ] : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// For a given absolute Rect coordinates object and a positioned element ancestor, it returns an object with
|
|
|
|
|
+// new Rect coordinates that make up for the position and the scroll of the ancestor.
|
|
|
|
|
+//
|
|
|
|
|
+// This is necessary because while Rects (and DOMRects) are relative to the browser's viewport, their coordinates
|
|
|
|
|
+// are used in real–life to position elements with `position: absolute`, which are scoped by any positioned
|
|
|
|
|
+// (and scrollable) ancestors.
|
|
|
|
|
+//
|
|
|
|
|
+// @private
|
|
|
|
|
+//
|
|
|
|
|
+// @param {Object} absoluteRectCoordinates An object with absolute rect coordinates.
|
|
|
|
|
+// @param {Object} absoluteRectCoordinates.top
|
|
|
|
|
+// @param {Object} absoluteRectCoordinates.left
|
|
|
|
|
+// @param {HTMLElement} positionedElementAncestor An ancestor element that should be considered.
|
|
|
|
|
+//
|
|
|
|
|
+// @returns {Object} An object corresponding to `absoluteRectCoordinates` input but with values shifted
|
|
|
|
|
+// to make up for the positioned element ancestor.
|
|
|
|
|
+function shiftRectCoordinatesDueToPositionedAncestor( { left, top }, positionedElementAncestor ) {
|
|
|
|
|
+ const ancestorPosition = getAbsoluteRectCoordinates( new Rect( positionedElementAncestor ) );
|
|
|
|
|
+ const ancestorBorderWidths = getBorderWidths( positionedElementAncestor );
|
|
|
|
|
+
|
|
|
|
|
+ // (https://github.com/ckeditor/ckeditor5-ui-default/issues/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.
|
|
|
|
|
+ left -= ancestorPosition.left;
|
|
|
|
|
+ top -= ancestorPosition.top;
|
|
|
|
|
+
|
|
|
|
|
+ // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
|
|
|
|
|
+ // If there's some positioned ancestor of the panel, not only its position must be taken into
|
|
|
|
|
+ // consideration (see above) but also its internal scrolls. Scroll have an impact here because `Rect`
|
|
|
|
|
+ // is relative to the viewport (it doesn't care about scrolling), while `position: absolute`
|
|
|
|
|
+ // must compensate that scrolling.
|
|
|
|
|
+ left += positionedElementAncestor.scrollLeft;
|
|
|
|
|
+ top += positionedElementAncestor.scrollTop;
|
|
|
|
|
+
|
|
|
|
|
+ // (https://github.com/ckeditor/ckeditor5-utils/issues/139)
|
|
|
|
|
+ // If there's some positioned ancestor of the panel, then its `Rect` includes its CSS `borderWidth`
|
|
|
|
|
+ // while `position: absolute` positioning does not consider it.
|
|
|
|
|
+ // E.g. `{ position: absolute, top: 0, left: 0 }` means upper left corner of the element,
|
|
|
|
|
+ // not upper-left corner of its border.
|
|
|
|
|
+ left -= ancestorBorderWidths.left;
|
|
|
|
|
+ top -= ancestorBorderWidths.top;
|
|
|
|
|
+
|
|
|
|
|
+ return { left, top };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't.
|
|
// DOMRect (also Rect) works in a scroll–independent geometry but `position: absolute` doesn't.
|
|
|
// This function converts Rect to `position: absolute` coordinates.
|
|
// This function converts Rect to `position: absolute` coordinates.
|
|
|
//
|
|
//
|