Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #106 from ckeditor/t/105
Browse files Browse the repository at this point in the history
t/105: Moved rect utilities from BalloonPanelView.
  • Loading branch information
Reinmar authored Dec 8, 2016
2 parents e4eae52 + 23072cf commit 5fe2e93
Show file tree
Hide file tree
Showing 8 changed files with 1,357 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/dom/getpositionedancestor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals window */

/**
* @module utils/dom/getpositionedancestor
*/

/**
* For a given element, returns the nearest ancestor element which CSS position is not "static".
*
* @param {HTMLElement} element Native DOM element to be checked.
* @returns {HTMLElement|null}
*/
export default function getPositionedAncestor( element ) {
while ( element && element.tagName.toLowerCase() != 'html' ) {
if ( window.getComputedStyle( element ).position != 'static' ) {
return element;
}

element = element.parentElement;
}

return null;
}
18 changes: 18 additions & 0 deletions src/dom/isrange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module utils/dom/isrange
*/

/**
* Checks if the object is a native DOM Range.
*
* @param {*} obj
* @returns {Boolean}
*/
export default function isRange( obj ) {
return Object.prototype.toString.apply( obj ) == '[object Range]';
}
272 changes: 272 additions & 0 deletions src/dom/position.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/**
* @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.
*
* // 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 <body>.
* 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 } ) {
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 = 0;
let maxViewportIntersectArea = 0;
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 ) {
// Consider only the part of the limiter which is visible in the viewport. So the limiter is getting limited.
const limiterViewportIntersectRect = limiterRect.getIntersection( viewportRect );

if ( limiterViewportIntersectRect ) {
// If the limiter is within the viewport, then check the intersection between that part of the
// limiter and actual position.
limiterIntersectArea = limiterViewportIntersectRect.getIntersectionArea( positionRect );
} else {
limiterIntersectArea = 0;
}
} 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} #element
*/

/**
* Target with respect to which the `element` is to be positioned.
*
* @member {HTMLElement|Range|ClientRect} #target
*/

/**
* An array of functions which return {@link module:utils/dom/position~Position} relative
* to the `target`, in the order of preference.
*
* @member {Array.<Function>} #positions
*/

/**
* When set, the algorithm will chose position which fits the most in the
* limiter's bounding rect.
*
* @member {HTMLElement|Range|ClientRect} #limiter
*/

/**
* When set, the algorithm will chose such a position which fits `element`
* the most inside visible viewport.
*
* @member {Boolean} #fitInViewport
*/

/**
* An object describing a position in `position: absolute` coordinate
* system, along with position name.
*
* @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.
*/
Loading

0 comments on commit 5fe2e93

Please sign in to comment.