8
0
Просмотр исходного кода

Implemented Rect class methods #isEqual() and #contains(). Allowed exclusion of borders and scrollbars.

Aleksander Nowodzinski 8 лет назад
Родитель
Сommit
ab167f71a9
2 измененных файлов с 301 добавлено и 6 удалено
  1. 93 6
      packages/ckeditor5-utils/src/dom/rect.js
  2. 208 0
      packages/ckeditor5-utils/tests/dom/rect.js

+ 93 - 6
packages/ckeditor5-utils/src/dom/rect.js

@@ -10,6 +10,7 @@
 import global from './global';
 import isRange from './isrange';
 import isElement from '../lib/lodash/isElement';
+import getBorderWidths from './getborderwidths';
 
 /**
  * A helper class representing a `ClientRect` object, e.g. value returned by
@@ -35,9 +36,17 @@ export default class Rect {
 	 *		// Rect out of a ClientRect.
 	 *		const rectE = new Rect( document.body.getClientRects().item( 0 ) );
 	 *
+	 * **Note**: By default `Rect` of `HTMLElement` includes its CSS borders and scrollbars (if any).
+	 * Use `options.excludeScrollbarsAndBorders` to obtain an "inner rect".
+	 *
+	 *		// Rect of an HTMLElement, scrollbars excluded.
+	 *		const rectF = new Rect( document.body, { excludeScrollbarsAndBorders: true } );
+	 *
 	 * @param {HTMLElement|Range|ClientRect|module:utils/dom/rect~Rect|Object} source A source object to create the rect.
+	 * @param {Boolean} [options.excludeScrollbarsAndBorders] When set `true` the `Rect` will not include
+	 * CSS borders and scrollbars. The option is valid for `HTMLElement` passed as a `source` only.
 	 */
-	constructor( source ) {
+	constructor( source, options = {} ) {
 		/**
 		 * The object this rect is for.
 		 *
@@ -46,14 +55,18 @@ export default class Rect {
 		 * @member {HTMLElement|Range|ClientRect|module:utils/dom/rect~Rect|Object} #_source
 		 */
 		Object.defineProperty( this, '_source', {
-			// source._source if already the Rect instance
+			// If the source is a Rect instance, copy it's #_source.
 			value: source._source || source,
-			writable: false,
+			writable: true,
 			enumerable: false
 		} );
 
 		if ( isElement( source ) ) {
 			copyRectProperties( this, source.getBoundingClientRect() );
+
+			if ( options.excludeScrollbarsAndBorders ) {
+				this._excludeScrollbarsAndBorders();
+			}
 		} else if ( isRange( source ) ) {
 			copyRectProperties( this, Rect.getDomRangeRects( source )[ 0 ] );
 		} else {
@@ -233,15 +246,49 @@ export default class Rect {
 		return visibleRect;
 	}
 
+	/**
+	 * Checks if all properties ({@link #top}, {@link #left}, {@link #right},
+	 * {@link #bottom}, {@link #width} and {@link #height}) are the same as in the other `Rect`.
+	 *
+	 * @param {Rect} rect A `Rect` instance to compare with.
+	 * @returns {Boolean} `true` when Rects are equal. `false` otherwise.
+	 */
+	isEqual( anotherRect ) {
+		for ( const prop of rectProperties ) {
+			if ( this[ prop ] !== anotherRect[ prop ] ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Checks whether a `Rect` fully contains another `Rect` instance.
+	 *
+	 * @param {module:utils/dom/rect~Rect} anotherRect
+	 * @returns {Boolean} `true` if contains, `false` otherwise.
+	 */
+	contains( anotherRect ) {
+		const intersectRect = this.getIntersection( anotherRect );
+
+		if ( !intersectRect || !intersectRect.isEqual( anotherRect ) ) {
+			return false;
+		}
+
+		return true;
+	}
+
 	/**
 	 * Returns a rect of the web browser viewport.
 	 *
 	 * @returns {module:utils/dom/rect~Rect} A viewport rect.
+	 * @param {Boolean} [options.excludeScrollbars] When set `true` the `Rect` will not include
+	 * the scrollbars of the viewport.
 	 */
-	static getViewportRect() {
+	static getViewportRect( options = {} ) {
 		const { innerWidth, innerHeight } = global.window;
-
-		return new Rect( {
+		const rect = new Rect( {
 			top: 0,
 			right: innerWidth,
 			bottom: innerHeight,
@@ -249,6 +296,14 @@ export default class Rect {
 			width: innerWidth,
 			height: innerHeight
 		} );
+
+		rect._source = global.window;
+
+		if ( options.excludeScrollbars ) {
+			rect._excludeScrollbarsAndBorders();
+		}
+
+		return rect;
 	}
 
 	/**
@@ -280,6 +335,38 @@ export default class Rect {
 
 		return rects;
 	}
+
+	/**
+	 * Excludes scrollbars and CSS borders from the `Rect`.
+	 *
+	 * * Borders are removed when {@link #_source} is `HTMLElement`.
+	 * * Scrollbars are excluded from `HTMLElements` and {@link #getViewportRect viewport rects}.
+	 *
+	 * @private
+	 */
+	_excludeScrollbarsAndBorders() {
+		const source = this._source;
+		let scrollBarWidth, scrollBarHeight;
+
+		if ( source === global.window ) {
+			scrollBarWidth = global.window.innerWidth - global.document.documentElement.clientWidth;
+			scrollBarHeight = global.window.innerHeight - global.document.documentElement.clientHeight;
+		} else {
+			const borderWidths = getBorderWidths( this._source );
+
+			scrollBarWidth = source.offsetWidth - source.clientWidth;
+			scrollBarHeight = source.offsetHeight - source.clientHeight;
+
+			this.moveBy( borderWidths.left, borderWidths.top );
+		}
+
+		// Assuming LTR scrollbars. TODO: RTL.
+		this.width -= scrollBarWidth;
+		this.right -= scrollBarWidth;
+
+		this.height -= scrollBarHeight;
+		this.bottom -= scrollBarHeight;
+	}
 }
 
 const rectProperties = [ 'top', 'right', 'bottom', 'left', 'width', 'height' ];

+ 208 - 0
packages/ckeditor5-utils/tests/dom/rect.js

@@ -41,6 +41,43 @@ describe( 'Rect', () => {
 			assertRect( new Rect( element ), geometry );
 		} );
 
+		it( 'should accept HTMLElement (excludeScrollbarsAndBorders)', () => {
+			const element = document.createElement( 'div' );
+
+			testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( geometry );
+			testUtils.sinon.stub( global.window, 'getComputedStyle' ).returns( {
+				borderTopWidth: '5px',
+				borderRightWidth: '10px',
+				borderLeftWidth: '5px',
+				borderBottomWidth: '10px'
+			} );
+
+			// Simulate 5px srollbars.
+			Object.defineProperties( element, {
+				offsetWidth: {
+					value: 20
+				},
+				offsetHeight: {
+					value: 20
+				},
+				clientWidth: {
+					value: 10
+				},
+				clientHeight: {
+					value: 10
+				}
+			} );
+
+			assertRect( new Rect( element, { excludeScrollbarsAndBorders: true } ), {
+				top: 15,
+				right: 35,
+				bottom: 25,
+				left: 25,
+				width: 10,
+				height: 10
+			} );
+		} );
+
 		it( 'should accept Range (non–collapsed)', () => {
 			const range = document.createRange();
 
@@ -562,6 +599,150 @@ describe( 'Rect', () => {
 		} );
 	} );
 
+	describe( 'isEqual()', () => {
+		it( 'returns `true` when rects are equal', () => {
+			const rectA = new Rect( {
+				top: 10,
+				right: 20,
+				bottom: 30,
+				left: 40,
+				width: 10,
+				height: 20
+			} );
+
+			const rectB = new Rect( {
+				top: 10,
+				right: 20,
+				bottom: 30,
+				left: 40,
+				width: 10,
+				height: 20
+			} );
+
+			expect( rectA.isEqual( rectB ) ).to.be.true;
+			expect( rectB.isEqual( rectA ) ).to.be.true;
+			expect( rectA.isEqual( rectA ) ).to.be.true;
+		} );
+
+		it( 'returns `false` when rects are different', () => {
+			const rectA = new Rect( {
+				top: 10,
+				right: 20,
+				bottom: 30,
+				left: 40,
+				width: 10,
+				height: 20
+			} );
+
+			const rectB = new Rect( {
+				top: 10,
+				right: 20,
+				bottom: 30,
+				left: 40,
+				width: 10,
+				height: 30 // !
+			} );
+
+			expect( rectA.isEqual( rectB ) ).to.be.false;
+			expect( rectB.isEqual( rectA ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'contains()', () => {
+		it( 'should return true if rects are the same', () => {
+			const rectA = new Rect( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			const rectB = new Rect( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			expect( rectA.isEqual( rectB ) ).to.be.true;
+			expect( rectA.contains( rectB ) ).to.be.true;
+			expect( rectB.contains( rectA ) ).to.be.true;
+		} );
+
+		it( 'should return true if rect is within', () => {
+			const rectA = new Rect( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			const rectB = new Rect( {
+				top: 10,
+				right: 90,
+				bottom: 90,
+				left: 10,
+				width: 80,
+				height: 80
+			} );
+
+			expect( rectA.contains( rectB ) ).to.be.true;
+			expect( rectB.contains( rectA ) ).to.be.false;
+		} );
+
+		it( 'should return false if rect extends beyond the boundaries', () => {
+			const rectA = new Rect( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			const rectB = new Rect( {
+				top: 10,
+				right: 100,
+				bottom: 110,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			expect( rectA.contains( rectB ) ).to.be.false;
+			expect( rectB.contains( rectA ) ).to.be.false;
+		} );
+
+		it( 'should return false if rect is completely beyond the boundaries', () => {
+			const rectA = new Rect( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			const rectB = new Rect( {
+				top: 110,
+				right: 100,
+				bottom: 210,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			expect( rectA.contains( rectB ) ).to.be.false;
+			expect( rectB.contains( rectA ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'getViewportRect()', () => {
 		it( 'should reaturn a rect', () => {
 			expect( Rect.getViewportRect() ).to.be.instanceOf( Rect );
@@ -584,6 +765,33 @@ describe( 'Rect', () => {
 				height: 500
 			} );
 		} );
+
+		describe( 'excludeScrollbars', () => {
+			it( 'should exclude scrollbars from viewport\'s rect', () => {
+				testUtils.sinon.stub( global, 'window', {
+					innerWidth: 1000,
+					innerHeight: 500,
+					scrollX: 100,
+					scrollY: 200
+				} );
+
+				testUtils.sinon.stub( global, 'document', {
+					documentElement: {
+						clientWidth: 990,
+						clientHeight: 490
+					}
+				} );
+
+				assertRect( Rect.getViewportRect( { excludeScrollbars: true } ), {
+					top: 0,
+					right: 990,
+					bottom: 490,
+					left: 0,
+					width: 990,
+					height: 490
+				} );
+			} );
+		} );
 	} );
 
 	describe( 'getDomRangeRects() ', () => {