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

Implemented Rect#getVisible method. Defined Rect#_obj property.

Aleksander Nowodzinski 8 лет назад
Родитель
Сommit
114e54bbb8
2 измененных файлов с 244 добавлено и 0 удалено
  1. 53 0
      packages/ckeditor5-utils/src/dom/rect.js
  2. 191 0
      packages/ckeditor5-utils/tests/dom/rect.js

+ 53 - 0
packages/ckeditor5-utils/src/dom/rect.js

@@ -40,6 +40,20 @@ export default class Rect {
 	 * @param {HTMLElement|Range|ClientRect|module:utils/dom/rect~Rect|Object} obj A source object to create the rect.
 	 */
 	constructor( obj ) {
+		/**
+		 * The object this rect is for.
+		 *
+		 * @protected
+		 * @readonly
+		 * @member {HTMLElement|Range|ClientRect|module:utils/dom/rect~Rect|Object} #_obj
+		 */
+		Object.defineProperty( this, '_obj', {
+			// obj._obj if already the Rect instance
+			value: obj._obj || obj,
+			writable: false,
+			enumerable: false
+		} );
+
 		if ( isElement( obj ) || isRange( obj ) ) {
 			obj = obj.getBoundingClientRect();
 		}
@@ -180,6 +194,45 @@ export default class Rect {
 	}
 
 	/**
+	 * Returns a new rect, a part of the original rect, which is actually visible to the user,
+	 * e.g. an original rect cropped by parent element rects which have `overflow` set in CSS
+	 * other than `"visible"`.
+	 *
+	 * If there's no such visible rect, which is when the rect is obscured by one or many of
+	 * the ancestors, `null` is returned.
+	 *
+	 * @returns {module:utils/dom/rect~Rect|null} A visible rect instance or `null`, if there's none.
+	 */
+	getVisible() {
+		const obj = this._obj;
+		let visibleRect = this.clone();
+		let parent = obj.parentNode || obj.commonAncestorContainer;
+
+		while ( parent && parent != global.document.body ) {
+			const parentOverflow = global.window.getComputedStyle( parent ).overflow;
+
+			if ( parentOverflow != 'visible' ) {
+				const parentRect = new Rect( parent );
+				const intersectionRect = visibleRect.getIntersection( parentRect );
+
+				if ( intersectionRect ) {
+					if ( intersectionRect.getArea() < visibleRect.getArea() ) {
+						// Reduce the visible rect to the intersection.
+						visibleRect = intersectionRect;
+					}
+				} else {
+					// There's no intersection, the rect is completely invisible.
+					return null;
+				}
+			}
+
+			parent = parent.parentNode;
+		}
+
+		return visibleRect;
+	}
+
+	/**
 	 * Returns a rect of the web browser viewport.
 	 *
 	 * @returns {module:utils/dom/rect~Rect} A viewport rect.

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

@@ -26,6 +26,13 @@ describe( 'Rect', () => {
 	} );
 
 	describe( 'constructor()', () => {
+		it( 'should store passed object in #_obj property', () => {
+			const obj = {};
+			const rect = new Rect( obj );
+
+			expect( rect._obj ).to.equal( obj );
+		} );
+
 		it( 'should accept HTMLElement', () => {
 			const element = document.createElement( 'div' );
 
@@ -97,6 +104,14 @@ describe( 'Rect', () => {
 			expect( clone ).not.equal( rect );
 			assertRect( clone, rect );
 		} );
+
+		it( 'should preserve #_obj', () => {
+			const rect = new Rect( geometry );
+			const clone = rect.clone();
+
+			expect( clone._obj ).to.equal( rect._obj );
+			assertRect( clone, rect );
+		} );
 	} );
 
 	describe( 'moveTo()', () => {
@@ -320,6 +335,182 @@ describe( 'Rect', () => {
 		} );
 	} );
 
+	describe( 'getVisible()', () => {
+		let element, range, ancestorA, ancestorB;
+
+		beforeEach( () => {
+			element = document.createElement( 'div' );
+			range = document.createRange();
+			ancestorA = document.createElement( 'div' );
+			ancestorB = document.createElement( 'div' );
+
+			ancestorA.append( element );
+			ancestorB.append( ancestorA );
+			document.body.appendChild( ancestorB );
+		} );
+
+		afterEach( () => {
+			ancestorA.remove();
+			ancestorB.remove();
+		} );
+
+		it( 'should return a new rect', () => {
+			const rect = new Rect( {} );
+			const visible = rect.getVisible();
+
+			expect( visible ).to.not.equal( rect );
+		} );
+
+		it( 'should return the visible rect (HTMLElement), partially cropped', () => {
+			ancestorA.style.overflow = 'scroll';
+
+			testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			testUtils.sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
+				top: 50,
+				right: 150,
+				bottom: 150,
+				left: 50,
+				width: 100,
+				height: 100
+			} );
+
+			assertRect( new Rect( element ).getVisible(), {
+				top: 50,
+				right: 100,
+				bottom: 100,
+				left: 50,
+				width: 50,
+				height: 50
+			} );
+		} );
+
+		it( 'should return the visible rect (HTMLElement), fully visible', () => {
+			ancestorA.style.overflow = 'scroll';
+
+			testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			testUtils.sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
+				top: 0,
+				right: 150,
+				bottom: 150,
+				left: 0,
+				width: 150,
+				height: 150
+			} );
+
+			assertRect( new Rect( element ).getVisible(), {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+		} );
+
+		it( 'should return the visible rect (HTMLElement), partially cropped, deep ancestor overflow', () => {
+			ancestorB.style.overflow = 'scroll';
+
+			testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			testUtils.sinon.stub( ancestorB, 'getBoundingClientRect' ).returns( {
+				top: 50,
+				right: 150,
+				bottom: 150,
+				left: 50,
+				width: 100,
+				height: 100
+			} );
+
+			assertRect( new Rect( element ).getVisible(), {
+				top: 50,
+				right: 100,
+				bottom: 100,
+				left: 50,
+				width: 50,
+				height: 50
+			} );
+		} );
+
+		it( 'should return the visible rect (Range), partially cropped', () => {
+			range.setStart( ancestorA, 0 );
+			ancestorA.style.overflow = 'scroll';
+
+			testUtils.sinon.stub( range, 'getBoundingClientRect' ).returns( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			testUtils.sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
+				top: 50,
+				right: 150,
+				bottom: 150,
+				left: 50,
+				width: 100,
+				height: 100
+			} );
+
+			assertRect( new Rect( range ).getVisible(), {
+				top: 50,
+				right: 100,
+				bottom: 100,
+				left: 50,
+				width: 50,
+				height: 50
+			} );
+		} );
+
+		it( 'should return null if there\'s no visible rect', () => {
+			ancestorA.style.overflow = 'scroll';
+
+			testUtils.sinon.stub( element, 'getBoundingClientRect' ).returns( {
+				top: 0,
+				right: 100,
+				bottom: 100,
+				left: 0,
+				width: 100,
+				height: 100
+			} );
+
+			testUtils.sinon.stub( ancestorA, 'getBoundingClientRect' ).returns( {
+				top: 150,
+				right: 200,
+				bottom: 200,
+				left: 150,
+				width: 50,
+				height: 50
+			} );
+
+			expect( new Rect( element ).getVisible() ).to.equal( null );
+		} );
+	} );
+
 	describe( 'getViewportRect()', () => {
 		it( 'should reaturn a rect', () => {
 			expect( Rect.getViewportRect() ).to.be.instanceOf( Rect );