Преглед изворни кода

Code refactoring and docs.

Aleksander Nowodzinski пре 5 година
родитељ
комит
c4521b683b
1 измењених фајлова са 97 додато и 60 уклоњено
  1. 97 60
      packages/ckeditor5-utils/src/dom/position.js

+ 97 - 60
packages/ckeditor5-utils/src/dom/position.js

@@ -94,54 +94,33 @@ export function getOptimalPosition( { element, target, positions, limiter, fitIn
 	const elementRect = new Rect( element );
 	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 ( !limiter && !fitInViewport ) {
-		[ name, bestPosition ] = getPositionNameAndRect( positions[ 0 ], targetRect, elementRect );
+		[ 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 } );
 
-		[ 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 ) {
-		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.
@@ -161,24 +140,31 @@ function getPositionNameAndRect( position, targetRect, elementRect ) {
 // 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.
+//
+// @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 getBestPosition( positions, targetRect, elementRect, limiterRect, viewportRect ) {
+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, targetRect, elementRect, limiterRect, viewportRect );
+	const processedPositions = processPositionsToAreas( positions, options );
 
 	// First let's check all positions that fully fit in the viewport.
 	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.
 		const bestPositionData = getBestOfProcessedPositions( processedPositionsInViewport, elementRectArea );
@@ -198,18 +184,21 @@ function getBestPosition( positions, targetRect, elementRect, limiterRect, viewp
 // 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 {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:
 // * {String} positionName Name 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} 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 = [];
 
 	// 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)
-// 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
 // @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 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
 		// and limiterIntersectArea plane (without sqrt because we are looking for max value).
 		const fitFactor = viewportIntersectArea ** 2 + limiterIntersectArea ** 2;
@@ -289,15 +284,57 @@ function getBestOfProcessedPositions( processedPositions, elementRectArea ) {
 			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;
 }
 
+// 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.
 // This function converts Rect to `position: absolute` coordinates.
 //