diff --git a/src/dom/getpositionedancestor.js b/src/dom/getpositionedancestor.js
new file mode 100644
index 0000000..8ea17c6
--- /dev/null
+++ b/src/dom/getpositionedancestor.js
@@ -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;
+}
diff --git a/src/dom/isrange.js b/src/dom/isrange.js
new file mode 100644
index 0000000..d65b286
--- /dev/null
+++ b/src/dom/isrange.js
@@ -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]';
+}
diff --git a/src/dom/position.js b/src/dom/position.js
new file mode 100644
index 0000000..e4e8602
--- /dev/null
+++ b/src/dom/position.js
@@ -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
.
+ * 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.} #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.
+ */
diff --git a/src/dom/rect.js b/src/dom/rect.js
new file mode 100644
index 0000000..c78d5b1
--- /dev/null
+++ b/src/dom/rect.js
@@ -0,0 +1,200 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global window */
+
+/**
+ * @module utils/dom/rect
+ */
+
+import isRange from './isrange.js';
+import isElement from '../../utils/lib/lodash/isElement.js';
+
+const rectProperties = [ 'top', 'right', 'bottom', 'left', 'width', 'height' ];
+
+/**
+ * A helper class representing a `ClientRect` object, e.g. value returned by
+ * the native `object.getBoundingClientRect()` method. Provides a set of methods
+ * to manipulate the rect and compare it against other `Rect` instances.
+ */
+export default class Rect {
+ /**
+ * Creates an instance of rect.
+ *
+ * // Rect of an HTMLElement.
+ * const rectA = new Rect( document.body );
+ *
+ * // Rect of a DOM Range.
+ * const rectB = new Rect( document.getSelection().getRangeAt( 0 ) );
+ *
+ * // Rect out of an object.
+ * const rectC = new Rect( { top: 0, right: 10, bottom: 10, left: 0, width: 10, height: 10 } );
+ *
+ * // Rect out of another Rect instance.
+ * const rectD = new Rect( rectC );
+ *
+ * // Rect out of a ClientRect.
+ * const rectE = new Rect( document.body.getClientRects().item( 0 ) );
+ *
+ * @param {HTMLElement|Range|ClientRect|module:utils/dom/rect~Rect|Object} obj A source object to create the rect.
+ */
+ constructor( obj ) {
+ if ( isElement( obj ) || isRange( obj ) ) {
+ obj = obj.getBoundingClientRect();
+ }
+
+ rectProperties.forEach( p => this[ p ] = obj[ p ] );
+
+ /**
+ * The "top" value of the rect.
+ *
+ * @readonly
+ * @member {Number} #top
+ */
+
+ /**
+ * The "right" value of the rect.
+ *
+ * @readonly
+ * @member {Number} #right
+ */
+
+ /**
+ * The "bottom" value of the rect.
+ *
+ * @readonly
+ * @member {Number} #bottom
+ */
+
+ /**
+ * The "left" value of the rect.
+ *
+ * @readonly
+ * @member {Number} #left
+ */
+
+ /**
+ * The "width" value of the rect.
+ *
+ * @readonly
+ * @member {Number} #width
+ */
+
+ /**
+ * The "height" value of the rect.
+ *
+ * @readonly
+ * @member {Number} #height
+ */
+ }
+
+ /**
+ * Returns a clone of the rect.
+ *
+ * @returns {module:utils/dom/rect~Rect} A cloned rect.
+ */
+ clone() {
+ return new Rect( this );
+ }
+
+ /**
+ * Moves the rect so that its upper–left corner lands in desired `[ x, y ]` location.
+ *
+ * @param {Number} x Desired horizontal location.
+ * @param {Number} y Desired vertical location.
+ * @returns {module:utils/dom/rect~Rect} A rect which has been moved.
+ */
+ moveTo( x, y ) {
+ this.top = y;
+ this.right = x + this.width;
+ this.bottom = y + this.height;
+ this.left = x;
+
+ return this;
+ }
+
+ /**
+ * Moves the rect in–place by a dedicated offset.
+ *
+ * @param {Number} x A horizontal offset.
+ * @param {Number} y A vertical offset
+ * @returns {module:utils/dom/rect~Rect} A rect which has been moved.
+ */
+ moveBy( x, y ) {
+ this.top += y;
+ this.right += x;
+ this.left += x;
+ this.bottom += y;
+
+ return this;
+ }
+
+ /**
+ * Returns a new rect a a result of intersection with another rect.
+ *
+ * @param {module:utils/dom/rect~Rect} anotherRect
+ * @returns {module:utils/dom/rect~Rect}
+ */
+ getIntersection( anotherRect ) {
+ const rect = {
+ top: Math.max( this.top, anotherRect.top ),
+ right: Math.min( this.right, anotherRect.right ),
+ bottom: Math.min( this.bottom, anotherRect.bottom ),
+ left: Math.max( this.left, anotherRect.left )
+ };
+
+ rect.width = rect.right - rect.left;
+ rect.height = rect.bottom - rect.top;
+
+ if ( rect.width < 0 || rect.height < 0 ) {
+ return null;
+ } else {
+ return new Rect( rect );
+ }
+ }
+
+ /**
+ * Returns the area of intersection with another rect.
+ *
+ * @param {module:utils/dom/rect~Rect} anotherRect [description]
+ * @returns {Number} Area of intersection.
+ */
+ getIntersectionArea( anotherRect ) {
+ const rect = this.getIntersection( anotherRect );
+
+ if ( rect ) {
+ return rect.getArea();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Returns the area of the rect.
+ *
+ * @returns {Number}
+ */
+ getArea() {
+ return this.width * this.height;
+ }
+
+ /**
+ * Returns a rect of the web browser viewport.
+ *
+ * @returns {module:utils/dom/rect~Rect} A viewport rect.
+ */
+ static getViewportRect() {
+ const { innerWidth, innerHeight } = window;
+
+ return new Rect( {
+ top: 0,
+ right: innerWidth,
+ bottom: innerHeight,
+ left: 0,
+ width: innerWidth,
+ height: innerHeight
+ } );
+ }
+}
diff --git a/tests/dom/getpositionedancestor.js b/tests/dom/getpositionedancestor.js
new file mode 100644
index 0000000..801766d
--- /dev/null
+++ b/tests/dom/getpositionedancestor.js
@@ -0,0 +1,54 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import getPositionedAncestor from 'ckeditor5/utils/dom/getpositionedancestor.js';
+
+describe( 'getPositionedAncestor', () => {
+ let element;
+
+ beforeEach( () => {
+ element = document.createElement( 'a' );
+
+ document.body.appendChild( element );
+ } );
+
+ it( 'should return null when there is no element', () => {
+ expect( getPositionedAncestor() ).to.be.null;
+ } );
+
+ it( 'should return null when there is no parent', () => {
+ expect( getPositionedAncestor( element ) ).to.be.null;
+ } );
+
+ it( 'should consider passed element', () => {
+ element.style.position = 'relative';
+
+ expect( getPositionedAncestor( element ) ).to.equal( element );
+ } );
+
+ it( 'should find the positioned ancestor (direct parent)', () => {
+ const parent = document.createElement( 'div' );
+
+ parent.appendChild( element );
+ document.body.appendChild( parent );
+ parent.style.position = 'absolute';
+
+ expect( getPositionedAncestor( element ) ).to.equal( parent );
+ } );
+
+ it( 'should find the positioned ancestor (far ancestor)', () => {
+ const parentA = document.createElement( 'div' );
+ const parentB = document.createElement( 'div' );
+
+ parentB.appendChild( element );
+ parentA.appendChild( parentB );
+ document.body.appendChild( parentA );
+ parentA.style.position = 'absolute';
+
+ expect( getPositionedAncestor( element ) ).to.equal( parentA );
+ } );
+} );
diff --git a/tests/dom/isrange.js b/tests/dom/isrange.js
new file mode 100644
index 0000000..7cf0d80
--- /dev/null
+++ b/tests/dom/isrange.js
@@ -0,0 +1,20 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global Range */
+
+import isRange from 'ckeditor5/utils/dom/isrange.js';
+
+describe( 'isRange()', () => {
+ it( 'detects native DOM Range', () => {
+ expect( isRange( new Range() ) ).to.be.true;
+
+ expect( isRange( {} ) ).to.be.false;
+ expect( isRange( null ) ).to.be.false;
+ expect( isRange( undefined ) ).to.be.false;
+ expect( isRange( new Date() ) ).to.be.false;
+ expect( isRange( 42 ) ).to.be.false;
+ } );
+} );
diff --git a/tests/dom/position.js b/tests/dom/position.js
new file mode 100644
index 0000000..fb9087b
--- /dev/null
+++ b/tests/dom/position.js
@@ -0,0 +1,418 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, window */
+
+import { getOptimalPosition } from 'ckeditor5/utils/dom/position.js';
+import Rect from 'ckeditor5/utils/dom/rect.js';
+import testUtils from 'tests/core/_utils/utils.js';
+
+testUtils.createSinonSandbox();
+
+let element, target, limiter, revertWindowScroll;
+
+describe( 'getOptimalPosition', () => {
+ beforeEach( () => {
+ // Give us a lot of space.
+ testUtils.sinon.stub( Rect, 'getViewportRect' ).returns( new Rect( {
+ top: 0,
+ right: 10000,
+ bottom: 10000,
+ left: 0,
+ width: 10000,
+ height: 10000
+ } ) );
+ } );
+
+ afterEach( () => {
+ if ( revertWindowScroll ) {
+ revertWindowScroll();
+ }
+ } );
+
+ describe( 'for single position', () => {
+ beforeEach( setElementTargetPlayground );
+
+ it( 'should return coordinates', () => {
+ assertPosition( { element, target, positions: [ attachLeft ] }, {
+ top: 100,
+ left: 80,
+ name: 'left'
+ } );
+ } );
+
+ it( 'should return coordinates (window scroll)', () => {
+ stubWindowScroll( 100, 100 );
+
+ assertPosition( { element, target, positions: [ attachLeft ] }, {
+ top: 200,
+ left: 180,
+ name: 'left'
+ } );
+ } );
+
+ it( 'should return coordinates (positioned element parent)', () => {
+ const positionedParent = document.createElement( 'div' );
+ stubWindowScroll( 1000, 1000 );
+
+ Object.assign( positionedParent.style, {
+ position: 'absolute',
+ top: '1000px',
+ left: '1000px'
+ } );
+
+ document.body.appendChild( positionedParent );
+ positionedParent.appendChild( element );
+
+ assertPosition( { element, target, positions: [ attachLeft ] }, {
+ top: -900,
+ left: -920,
+ name: 'left'
+ } );
+ } );
+ } );
+
+ describe( 'for multiple positions', () => {
+ beforeEach( setElementTargetPlayground );
+
+ it( 'should return coordinates', () => {
+ assertPosition( {
+ element, target,
+ positions: [ attachLeft, attachRight ]
+ }, {
+ top: 100,
+ left: 80,
+ name: 'left'
+ } );
+ } );
+
+ it( 'should return coordinates (position preference order)', () => {
+ assertPosition( {
+ element, target,
+ positions: [ attachRight, attachLeft ]
+ }, {
+ top: 100,
+ left: 110,
+ name: 'right'
+ } );
+ } );
+ } );
+
+ describe( 'with a limiter', () => {
+ beforeEach( setElementTargetLimiterPlayground );
+
+ it( 'should return coordinates (#1)', () => {
+ assertPosition( {
+ element, target, limiter,
+ positions: [ attachLeft, attachRight ]
+ }, {
+ top: 100,
+ left: -20,
+ name: 'left'
+ } );
+ } );
+
+ it( 'should return coordinates (#2)', () => {
+ assertPosition( {
+ element, target, limiter,
+ positions: [ attachRight, attachLeft ]
+ }, {
+ top: 100,
+ left: -20,
+ name: 'left'
+ } );
+ } );
+ } );
+
+ describe( 'with fitInViewport on', () => {
+ beforeEach( setElementTargetLimiterPlayground );
+
+ it( 'should return coordinates (#1)', () => {
+ assertPosition( {
+ element, target,
+ positions: [ attachLeft, attachRight ],
+ fitInViewport: true
+ }, {
+ top: 100,
+ left: 10,
+ name: 'right'
+ } );
+ } );
+
+ it( 'should return coordinates (#2)', () => {
+ assertPosition( {
+ element, target,
+ positions: [ attachRight, attachLeft ],
+ fitInViewport: true
+ }, {
+ top: 100,
+ left: 10,
+ name: 'right'
+ } );
+ } );
+
+ it( 'should return coordinates (#3)', () => {
+ assertPosition( {
+ element, target,
+ positions: [ attachLeft, attachBottom, attachRight ],
+ fitInViewport: true
+ }, {
+ top: 110,
+ left: 0,
+ name: 'bottom'
+ } );
+ } );
+ } );
+
+ describe( 'with limiter and fitInViewport on', () => {
+ beforeEach( setElementTargetLimiterPlayground );
+
+ it( 'should return coordinates (#1)', () => {
+ assertPosition( {
+ element, target, limiter,
+ positions: [ attachLeft, attachRight ],
+ fitInViewport: true
+ }, {
+ top: 100,
+ left: 10,
+ name: 'right'
+ } );
+ } );
+
+ it( 'should return coordinates (#2)', () => {
+ assertPosition( {
+ element, target, limiter,
+ positions: [ attachRight, attachLeft ],
+ fitInViewport: true
+ }, {
+ top: 100,
+ left: 10,
+ name: 'right'
+ } );
+ } );
+
+ it( 'should return coordinates (#3)', () => {
+ assertPosition( {
+ element, target, limiter,
+ positions: [ attachRight, attachLeft, attachBottom ],
+ fitInViewport: true
+ }, {
+ top: 110,
+ left: 0,
+ name: 'bottom'
+ } );
+ } );
+
+ it( 'should return coordinates (#4)', () => {
+ assertPosition( {
+ element, target, limiter,
+ positions: [ attachTop, attachRight ],
+ fitInViewport: true
+ }, {
+ top: 100,
+ left: 10,
+ name: 'right'
+ } );
+ } );
+
+ it( 'should return the very first coordinates if no fitting position with a positive intersection has been found', () => {
+ assertPosition( {
+ element, target, limiter,
+ positions: [
+ () => ( {
+ left: -10000,
+ top: -10000,
+ name: 'no-intersect-position'
+ } )
+ ],
+ fitInViewport: true
+ }, {
+ left: -10000,
+ top: -10000,
+ name: 'no-intersect-position'
+ } );
+ } );
+
+ it( 'should return the very first coordinates if limiter does not fit into the viewport', () => {
+ stubElementRect( limiter, {
+ top: -100,
+ right: -80,
+ bottom: -80,
+ left: -100,
+ width: 20,
+ height: 20
+ } );
+
+ assertPosition( {
+ element, target, limiter,
+ positions: [ attachRight, attachTop ],
+ fitInViewport: true
+ }, {
+ top: 100,
+ left: 10,
+ name: 'right'
+ } );
+ } );
+ } );
+} );
+
+function assertPosition( options, expected ) {
+ const position = getOptimalPosition( options );
+
+ expect( position ).to.deep.equal( expected );
+}
+
+// +--------+-----+
+// | E | T |
+// +--------+-----+
+const attachLeft = ( targetRect, elementRect ) => ( {
+ top: targetRect.top,
+ left: targetRect.left - elementRect.width,
+ name: 'left'
+} );
+
+// +-----+--------+
+// | T | E |
+// +-----+--------+
+const attachRight = ( targetRect ) => ( {
+ top: targetRect.top,
+ left: targetRect.left + targetRect.width,
+ name: 'right'
+} );
+
+// +-----+
+// | T |
+// +-----+--+
+// | E |
+// +--------+
+const attachBottom = ( targetRect ) => ( {
+ top: targetRect.bottom,
+ left: targetRect.left,
+ name: 'bottom'
+} );
+
+// +--------+
+// | E |
+// +--+-----+
+// | T |
+// +-----+
+const attachTop = ( targetRect, elementRect ) => ( {
+ top: targetRect.top - elementRect.height,
+ left: targetRect.left - ( elementRect.width - targetRect.width ),
+ name: 'bottom'
+} );
+
+function stubWindowScroll( x, y ) {
+ const { scrollX: savedX, scrollY: savedY } = window;
+
+ window.scrollX = x;
+ window.scrollY = y;
+
+ revertWindowScroll = () => {
+ window.scrollX = savedX;
+ window.scrollY = savedY;
+
+ revertWindowScroll = null;
+ };
+}
+
+function stubElementRect( element, rect ) {
+ if ( element.getBoundingClientRect.restore ) {
+ element.getBoundingClientRect.restore();
+ }
+
+ testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( rect );
+}
+
+// <-- 100px ->
+//
+// ^ +--------------[ Viewport ]----------
+// | |
+// 100px |
+// | | <-- 10px -->
+// V |
+// | ^ +---------+
+// | | | |
+// | 10px | T |
+// | | | |
+// | V +---------+
+// |
+//
+function setElementTargetPlayground() {
+ element = document.createElement( 'div' );
+ target = document.createElement( 'div' );
+
+ stubElementRect( element, {
+ top: 0,
+ right: 20,
+ bottom: 20,
+ left: 0,
+ width: 20,
+ height: 20
+ } );
+
+ stubElementRect( target, {
+ top: 100,
+ right: 110,
+ bottom: 110,
+ left: 100,
+ width: 10,
+ height: 10
+ } );
+}
+
+//
+//
+// ^ +-----------[ Viewport ]----------------------
+// | |
+// 100px |
+// | <--------- 20px ------->
+// | <-- 10px -->
+// V |
+// +------------+---------+ ^ ^
+// | | | | |
+// | | T | 10px |
+// | | | | |
+// | +---------+ V 20px
+// | | | |
+// | | | |
+// | | | |
+// +------[ Limiter ]-----+ V
+// |
+// |
+//
+//
+function setElementTargetLimiterPlayground() {
+ element = document.createElement( 'div' );
+ target = document.createElement( 'div' );
+ limiter = document.createElement( 'div' );
+
+ stubElementRect( element, {
+ top: 0,
+ right: 20,
+ bottom: 20,
+ left: 0,
+ width: 20,
+ height: 20
+ } );
+
+ stubElementRect( limiter, {
+ top: 100,
+ right: 10,
+ bottom: 120,
+ left: -10,
+ width: 20,
+ height: 20
+ } );
+
+ stubElementRect( target, {
+ top: 100,
+ right: 10,
+ bottom: 110,
+ left: 0,
+ width: 10,
+ height: 10
+ } );
+}
diff --git a/tests/dom/rect.js b/tests/dom/rect.js
new file mode 100644
index 0000000..a2e7b7b
--- /dev/null
+++ b/tests/dom/rect.js
@@ -0,0 +1,347 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, window */
+
+import Rect from 'ckeditor5/utils/dom/rect.js';
+import testUtils from 'tests/core/_utils/utils.js';
+
+testUtils.createSinonSandbox();
+
+describe( 'Rect', () => {
+ let geometry;
+
+ beforeEach( () => {
+ geometry = {
+ top: 10,
+ right: 40,
+ bottom: 30,
+ left: 20,
+ width: 20,
+ height: 20
+ };
+ } );
+
+ describe( 'constructor()', () => {
+ it( 'should accept HTMLElement', () => {
+ const element = document.createElement( 'div' );
+
+ testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
+
+ assertRect( new Rect( element ), geometry );
+ } );
+
+ it( 'should accept Range', () => {
+ const range = document.createRange();
+
+ testUtils.sinon.stub( range, 'getBoundingClientRect' ).returns( geometry );
+
+ assertRect( new Rect( range ), geometry );
+ } );
+
+ it( 'should accept Rect', () => {
+ const sourceRect = new Rect( geometry );
+ const rect = new Rect( sourceRect );
+
+ expect( rect ).to.not.equal( sourceRect );
+ assertRect( rect, geometry );
+ } );
+
+ it( 'should accept ClientRect', () => {
+ const clientRect = document.body.getBoundingClientRect();
+ const { top, right, bottom, left, width, height } = clientRect;
+ const rect = new Rect( clientRect );
+
+ assertRect( rect, { top, right, bottom, left, width, height } );
+ } );
+
+ it( 'should accept geometry object', () => {
+ assertRect( new Rect( geometry ), geometry );
+ } );
+
+ it( 'should copy the properties (Rect)', () => {
+ const sourceGeometry = Object.assign( {}, geometry );
+ const sourceRect = new Rect( geometry );
+ const rect = new Rect( sourceRect );
+
+ assertRect( rect, geometry );
+
+ rect.top = 100;
+ rect.width = 200;
+
+ assertRect( sourceRect, sourceGeometry );
+ } );
+
+ it( 'should copy the properties (geomerty object)', () => {
+ const sourceGeometry = Object.assign( {}, geometry );
+ const rect = new Rect( geometry );
+
+ assertRect( rect, geometry );
+
+ rect.top = 100;
+ rect.width = 200;
+
+ assertRect( geometry, sourceGeometry );
+ } );
+ } );
+
+ describe( 'clone()', () => {
+ it( 'should clone the source rect', () => {
+ const rect = new Rect( geometry );
+ const clone = rect.clone();
+
+ expect( clone ).to.be.instanceOf( Rect );
+ expect( clone ).not.equal( rect );
+ assertRect( clone, rect );
+ } );
+ } );
+
+ describe( 'moveTo()', () => {
+ it( 'should return the source rect', () => {
+ const rect = new Rect( geometry );
+ const returned = rect.moveTo( 100, 200 );
+
+ expect( returned ).to.equal( rect );
+ } );
+
+ it( 'should move the rect', () => {
+ const rect = new Rect( geometry );
+
+ rect.moveTo( 100, 200 );
+
+ assertRect( rect, {
+ top: 200,
+ right: 120,
+ bottom: 220,
+ left: 100,
+ width: 20,
+ height: 20
+ } );
+ } );
+ } );
+
+ describe( 'moveBy()', () => {
+ it( 'should return the source rect', () => {
+ const rect = new Rect( geometry );
+ const returned = rect.moveBy( 100, 200 );
+
+ expect( returned ).to.equal( rect );
+ } );
+
+ it( 'should move the rect', () => {
+ const rect = new Rect( geometry );
+
+ rect.moveBy( 100, 200 );
+
+ assertRect( rect, {
+ top: 210,
+ right: 140,
+ bottom: 230,
+ left: 120,
+ width: 20,
+ height: 20
+ } );
+ } );
+ } );
+
+ describe( 'getIntersection()', () => {
+ it( 'should return a new rect', () => {
+ const rect = new Rect( geometry );
+ const insersect = rect.getIntersection( new Rect( geometry ) );
+
+ expect( insersect ).to.be.instanceOf( Rect );
+ expect( insersect ).to.not.equal( rect );
+ } );
+
+ it( 'should calculate the geometry (#1)', () => {
+ const rectA = new Rect( {
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
+ width: 100,
+ height: 100
+ } );
+
+ const rectB = new Rect( {
+ top: 50,
+ right: 150,
+ bottom: 150,
+ left: 50,
+ width: 100,
+ height: 100
+ } );
+
+ const insersect = rectA.getIntersection( rectB );
+
+ assertRect( insersect, {
+ top: 50,
+ right: 100,
+ bottom: 100,
+ left: 50,
+ width: 50,
+ height: 50
+ } );
+ } );
+
+ it( 'should calculate the geometry (#2)', () => {
+ const rectA = new Rect( {
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
+ width: 100,
+ height: 100
+ } );
+
+ const rectB = new Rect( {
+ top: 0,
+ right: 200,
+ bottom: 100,
+ left: 100,
+ width: 100,
+ height: 100
+ } );
+
+ const insersect = rectA.getIntersection( rectB );
+
+ assertRect( insersect, {
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 100,
+ width: 0,
+ height: 100
+ } );
+ } );
+
+ it( 'should calculate the geometry (#3)', () => {
+ const rectA = new Rect( {
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
+ width: 100,
+ height: 100
+ } );
+
+ const rectB = new Rect( {
+ top: 100,
+ right: 300,
+ bottom: 200,
+ left: 200,
+ width: 100,
+ height: 100
+ } );
+
+ expect( rectA.getIntersection( rectB ) ).to.be.null;
+ } );
+ } );
+
+ describe( 'getIntersectionArea()', () => {
+ it( 'should calculate the area (#1)', () => {
+ const rectA = new Rect( {
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
+ width: 100,
+ height: 100
+ } );
+
+ const rectB = new Rect( {
+ top: 50,
+ right: 150,
+ bottom: 150,
+ left: 50,
+ width: 100,
+ height: 100
+ } );
+
+ expect( rectA.getIntersectionArea( rectB ) ).to.equal( 2500 );
+ } );
+
+ it( 'should calculate the area (#2)', () => {
+ const rectA = new Rect( {
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
+ width: 100,
+ height: 100
+ } );
+
+ const rectB = new Rect( {
+ top: 0,
+ right: 200,
+ bottom: 100,
+ left: 100,
+ width: 100,
+ height: 100
+ } );
+
+ expect( rectA.getIntersectionArea( rectB ) ).to.equal( 0 );
+ } );
+
+ it( 'should calculate the area (#3)', () => {
+ const rectA = new Rect( {
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
+ width: 100,
+ height: 100
+ } );
+
+ const rectB = new Rect( {
+ top: 100,
+ right: 300,
+ bottom: 200,
+ left: 200,
+ width: 100,
+ height: 100
+ } );
+
+ expect( rectA.getIntersectionArea( rectB ) ).to.equal( 0 );
+ } );
+ } );
+
+ describe( 'getArea()', () => {
+ it( 'should calculate the area', () => {
+ const rect = new Rect( {
+ width: 100,
+ height: 50
+ } );
+
+ expect( rect.getArea() ).to.equal( 5000 );
+ } );
+ } );
+
+ describe( 'getViewportRect()', () => {
+ it( 'should reaturn a rect', () => {
+ expect( Rect.getViewportRect() ).to.be.instanceOf( Rect );
+ } );
+
+ it( 'should return the viewport\'s rect', () => {
+ window.scrollX = 100;
+ window.scrollY = 200;
+ window.innerWidth = 1000;
+ window.innerHeight = 500;
+
+ assertRect( Rect.getViewportRect(), {
+ top: 0,
+ right: 1000,
+ bottom: 500,
+ left: 0,
+ width: 1000,
+ height: 500
+ } );
+ } );
+ } );
+} );
+
+function assertRect( rect, expected ) {
+ expect( rect ).to.deep.equal( expected );
+}