Explorar el Código

Implemented the `centeredBalloonPositionForLongWidgets()` helper to better position balloons around widgets that stick off the viewport.

Aleksander Nowodzinski hace 5 años
padre
commit
cf11d8eea6
Se han modificado 2 ficheros con 177 adiciones y 1 borrados
  1. 73 0
      packages/ckeditor5-widget/src/utils.js
  2. 104 1
      packages/ckeditor5-widget/tests/utils.js

+ 73 - 0
packages/ckeditor5-widget/src/utils.js

@@ -9,6 +9,9 @@
 
 import HighlightStack from './highlightstack';
 import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 
 import dragHandleIcon from '../theme/icons/drag-handle.svg';
 
@@ -339,6 +342,76 @@ export function viewToModelPositionOutsideModelElement( model, viewElementMatche
 	};
 }
 
+/**
+ * A positioning function passed to the {@link module:utils/dom/position~getOptimalPosition} helper as a last resort
+ * when attaching {@link  module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon UI} to widgets.
+ * It comes in handy when a widget is longer than the visual viewport of the web browser and/or upper/lower boundaries
+ * of a widget are off screen because of the web page scroll.
+ *
+ *	                (A)                                 (B)                                 (C)
+ *
+ *	                                                                            ┌─┄┄┄┄┄┄┄┄Widget┄┄┄┄┄┄┄┄┄┄┐
+ *	                                                                            ┊                         ┊
+ *	                                        ┌─┄┄┄┄┄┄┄┄┄Widget┄┄┄┄┄┄┄┄┄┐         ┊                         ┊
+ *                                          ┊                         ┊         ┊                         ┊
+ *	 ┌────────────Viewport───────────┐   ┌──╁─────────Viewport────────╁──┐   ┌──╁────────Viewport─────────╁──┐
+ *	 │  ┏━━━━━━━━━━Widget━━━━━━━━━┓  │   │  ┃            ^            ┃  │   │  ┃                         ┃  │
+ *	 │  ┃            ^            ┃  │   │  ┃   ╭───────/ \───────╮   ┃  │   │  ┃                         ┃  │
+ *	 │  ┃   ╭───────/ \───────╮   ┃  │   │  ┃   │     Balloon     │   ┃  │   │  ┃                         ┃  │
+ *	 │  ┃   │     Balloon     │   ┃  │   │  ┃   ╰─────────────────╯   ┃  │   │  ┃                         ┃  │
+ *	 │  ┃   ╰─────────────────╯   ┃  │   │  ┃                         ┃  │   │  ┃                         ┃  │
+ *	 │  ┃                         ┃  │   │  ┃                         ┃  │   │  ┃                         ┃  │
+ *	 │  ┃                         ┃  │   │  ┃                         ┃  │   │  ┃   ╭─────────────────╮   ┃  │
+ *	 │  ┃                         ┃  │   │  ┃                         ┃  │   │  ┃   │     Balloon     │   ┃  │
+ *	 │  ┃                         ┃  │   │  ┃                         ┃  │   │  ┃   ╰───────\ /───────╯   ┃  │
+ *	 │  ┃                         ┃  │   │  ┃                         ┃  │   │  ┃            V            ┃  │
+ *	 │  ┃                         ┃  │   │  ┃                         ┃  │   │  ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛  │
+ *	 └──╀─────────────────────────╀──┘   └──╀─────────────────────────╀──┘   └───────────────────────────────┘
+ *	    ┊                         ┊         ┊                         ┊
+ *	    ┊                         ┊         └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+ *	    ┊                         ┊
+ *	    └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
+ *
+ *
+ * **Note**: Works best if used together with
+ * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions default `BalloonPanelView` positions}
+ * like `northArrowSouth` and `southArrowNorth`; the transition between these two and this position is smooth.
+ *
+ * @param {utils/dom/rect~Rect} widgetRect A rect of the widget.
+ * @param {utils/dom/rect~Rect} balloonRect A rect of the balloon.
+ * @returns {module:utils/dom/position~Position}
+ */
+export function centeredBalloonPositionForLongWidgets( widgetRect, balloonRect ) {
+	const viewportRect = new Rect( global.window );
+	const viewportWidgetInsersectionRect = viewportRect.getIntersection( widgetRect );
+	const isUpperTargetEdgeOffViewport = widgetRect.top < viewportRect.top;
+	const isLowerTargetEdgeOffViewport = widgetRect.bottom > viewportRect.bottom;
+
+	// Because this is a last resort positioning, to keep things simple we're not playing with positions of the arrow
+	// like, for instance, "south west" or whatever. Just try to keep the balloon in the middle of the visible area of
+	// the widget for as long as it is possible. If the widgets becomes invisible (because cropped by the viewport),
+	// just... place the balloon in the middle of it (because why not?).
+	const targetRect = viewportWidgetInsersectionRect.width > 0 ? viewportWidgetInsersectionRect : widgetRect;
+	const left = targetRect.left + targetRect.width / 2 - balloonRect.width / 2;
+
+	// Case (C).
+	if ( isUpperTargetEdgeOffViewport && !isLowerTargetEdgeOffViewport ) {
+		return {
+			top: Math.min( widgetRect.bottom, viewportRect.bottom ) - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
+			left,
+			name: 'arrow_s'
+		};
+	}
+	// Cases (A) and (B).
+	else {
+		return {
+			top: Math.max( widgetRect.top, 0 ) + BalloonPanelView.arrowVerticalOffset,
+			left,
+			name: 'arrow_n'
+		};
+	}
+}
+
 // Default filler offset function applied to all widget elements.
 //
 // @returns {null}

+ 104 - 1
packages/ckeditor5-widget/tests/utils.js

@@ -20,7 +20,8 @@ import {
 	setHighlightHandling,
 	findOptimalInsertionPosition,
 	viewToModelPositionOutsideModelElement,
-	WIDGET_CLASS_NAME
+	WIDGET_CLASS_NAME,
+	centeredBalloonPositionForLongWidgets
 } from '../src/utils';
 import UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
@@ -29,6 +30,9 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import Mapper from '@ckeditor/ckeditor5-engine/src/conversion/mapper';
 import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
 import ModelText from '@ckeditor/ckeditor5-engine/src/model/text';
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 
 describe( 'widget utils', () => {
 	let element, writer, viewDocument;
@@ -489,4 +493,103 @@ describe( 'widget utils', () => {
 			expect( modelPosition.path ).to.deep.equal( [ 3, 1 ] );
 		} );
 	} );
+
+	describe( 'centeredBalloonPositionForLongWidgets()', () => {
+		const arrowVerticalOffset = BalloonPanelView.arrowVerticalOffset;
+
+		// Balloon is a 10x10 rect.
+		const balloonRect = new Rect( {
+			top: 0,
+			left: 0,
+			right: 10,
+			bottom: 10,
+			width: 10,
+			height: 10
+		} );
+
+		beforeEach( () => {
+			testUtils.sinon.stub( global.window, 'innerWidth' ).value( 100 );
+			testUtils.sinon.stub( global.window, 'innerHeight' ).value( 100 );
+		} );
+
+		it( 'should position the balloon inside a widget – at the top + in the middla', () => {
+			// Widget is a 50x150 rect, translated (25,25) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: 25,
+				left: 25,
+				right: 75,
+				bottom: 175,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: 25 + arrowVerticalOffset,
+				left: 45,
+				name: 'arrow_n'
+			} );
+		} );
+
+		it( 'should stick the balloon to the top of the viewport when the top (just as the bottom) of a widget is off-screen', () => {
+			// Widget is a 50x150 rect, translated (25,-25) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: -25,
+				left: 25,
+				right: 75,
+				bottom: 150,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: arrowVerticalOffset,
+				left: 45,
+				name: 'arrow_n'
+			} );
+		} );
+
+		it( 'should position the balloon inside a widget at the bottom if the top of a widget is off-screen (but bottom is not)', () => {
+			// Widget is a 50x150 rect, translated (25,-100) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: -100,
+				left: 25,
+				right: 75,
+				bottom: 50,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: 50 - balloonRect.height - arrowVerticalOffset,
+				left: 45,
+				name: 'arrow_s'
+			} );
+		} );
+
+		it( 'should horizontally center the balloon in the visible area when the widget is cropped by the viewport', () => {
+			// Widget is a 50x150 rect, translated (25,-25) from viewport's beginning (0,0).
+			const widgetRect = new Rect( {
+				top: 25,
+				left: -25,
+				right: 25,
+				bottom: 175,
+				width: 50,
+				height: 150
+			} );
+
+			const position = centeredBalloonPositionForLongWidgets( widgetRect, balloonRect );
+
+			expect( position ).to.deep.equal( {
+				top: 25 + arrowVerticalOffset,
+				left: 7.5,
+				name: 'arrow_n'
+			} );
+		} );
+	} );
 } );