8
0
Quellcode durchsuchen

Add tests for positioning the balloon panel for mentions.

Maciej Gołaszewski vor 6 Jahren
Ursprung
Commit
110aaddadb

+ 51 - 19
packages/ckeditor5-mention/src/mentionui.js

@@ -11,13 +11,15 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ListItemView from '@ckeditor/ckeditor5-ui/src/list/listitemview';
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
-import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 
 import MentionsView from './ui/mentionsview';
 import TextWatcher from './textwatcher';
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+
+const VERTICAL_SPACING = 5;
 
 /**
  * The mention ui feature.
@@ -225,7 +227,7 @@ export default class MentionUI extends Plugin {
 	}
 
 	_showPanel() {
-		this.panelView.pin( this._getBalloonPositionData() );
+		this.panelView.pin( this._getBalloonPanelPositionData() );
 		this.panelView.show();
 		this._mentionsView.selectFirst();
 	}
@@ -235,39 +237,69 @@ export default class MentionUI extends Plugin {
 		this.panelView.hide();
 	}
 
-	// TODO copied from balloontoolbar
-	_getBalloonPositionData() {
+	/**
+	 * @return {module:utils/dom/position~Options}
+	 * @private
+	 */
+	_getBalloonPanelPositionData() {
 		const editor = this.editor;
 		const view = editor.editing.view;
+		const domConverter = view.domConverter;
 		const viewDocument = view.document;
 		const viewSelection = viewDocument.selection;
 
 		return {
-			// Because the target for BalloonPanelView is a Rect (not DOMRange), it's geometry will stay fixed
-			// as the window scrolls. To let the BalloonPanelView follow such Rect, is must be continuously
-			// computed and hence, the target is defined as a function instead of a static value.
-			// https://github.com/ckeditor/ckeditor5-ui/issues/195
 			target: () => {
 				const range = viewSelection.getLastRange();
-				const rangeRects = Rect.getDomRangeRects( view.domConverter.viewRangeToDom( range ) );
-
-				if ( rangeRects.length > 1 && rangeRects[ rangeRects.length - 1 ].width === 0 ) {
-					rangeRects.pop();
-				}
+				const rangeRects = Rect.getDomRangeRects( domConverter.viewRangeToDom( range ) );
 
 				return rangeRects[ rangeRects.length - 1 ];
 			},
-			positions: getBalloonPositions()
+			positions: getBalloonPanelPositions()
 		};
 	}
 }
 
-function getBalloonPositions() {
-	const defaultPositions = BalloonPanelView.defaultPositions;
-
+// Returns balloon positions data callbacks.
+//
+// @returns {Array.<module:utils/dom/position~Position>}
+function getBalloonPanelPositions() {
 	return [
-		defaultPositions.southArrowNorthWest,
-		defaultPositions.northArrowSouthWest
+		// Positions panel to the south of caret rect.
+		targetRect => {
+			return {
+				top: targetRect.bottom + VERTICAL_SPACING,
+				left: targetRect.right,
+				name: 'caret_se'
+			};
+		},
+
+		// Positions panel to the north of caret rect.
+		( targetRect, balloonRect ) => {
+			return {
+				top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
+				left: targetRect.right,
+				name: 'caret_ne'
+			};
+		},
+
+		// Positions panel to the south of caret rect.
+		( targetRect, balloonRect ) => {
+			return {
+				top: targetRect.bottom + VERTICAL_SPACING,
+				left: targetRect.right - balloonRect.width,
+				name: 'caret_sw'
+			};
+		},
+
+		// Positions panel to the north of caret rect.
+		( targetRect, balloonRect ) => {
+			return {
+				top: targetRect.top - balloonRect.height - VERTICAL_SPACING,
+				left: targetRect.right - balloonRect.width,
+				name: 'caret_nw'
+			};
+		}
 	];
 }
 

+ 109 - 3
packages/ckeditor5-mention/tests/mentionui.js

@@ -80,6 +80,101 @@ describe( 'MentionUI', () => {
 		} );
 	} );
 
+	describe( 'position', () => {
+		let pinSpy;
+
+		const caretRect = {
+			bottom: 118,
+			height: 18,
+			left: 500,
+			right: 501,
+			top: 100,
+			width: 1
+		};
+
+		const balloonRect = {
+			bottom: 150,
+			height: 150,
+			left: 0,
+			right: 200,
+			top: 0,
+			width: 200
+		};
+
+		beforeEach( () => {
+			return createClassicTestEditor( staticConfig ).then( () => {
+				pinSpy = sinon.spy( panelView, 'pin' );
+			} );
+		} );
+
+		it( 'should properly calculate position data', () => {
+			setData( model, '<paragraph>foo []</paragraph>' );
+			stubSelectionRects( [ caretRect ] );
+
+			model.change( writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			return waitForDebounce()
+				.then( () => {
+					const pinArgument = pinSpy.firstCall.args[ 0 ];
+					const { target, positions } = pinArgument;
+
+					expect( target() ).to.deep.equal( caretRect );
+					expect( positions ).to.have.length( 4 );
+
+					const caretSouthEast = positions[ 0 ];
+					const caretNorthEast = positions[ 1 ];
+					const caretSouthWest = positions[ 2 ];
+					const caretNorthWest = positions[ 3 ];
+
+					expect( caretSouthEast( caretRect, balloonRect ) ).to.deep.equal( {
+						left: 501,
+						name: 'caret_se',
+						top: 123
+					} );
+
+					expect( caretNorthEast( caretRect, balloonRect ) ).to.deep.equal( {
+						left: 501,
+						name: 'caret_ne',
+						top: -55
+					} );
+
+					expect( caretSouthWest( caretRect, balloonRect ) ).to.deep.equal( {
+						left: 301,
+						name: 'caret_sw',
+						top: 123
+					} );
+
+					expect( caretNorthWest( caretRect, balloonRect ) ).to.deep.equal( {
+						left: 301,
+						name: 'caret_nw',
+						top: -55
+					} );
+				} );
+		} );
+
+		it( 'should re-calculate position on typing', () => {
+			setData( model, '<paragraph>foo []</paragraph>' );
+			stubSelectionRects( [ caretRect ] );
+
+			model.change( writer => {
+				writer.insertText( '@', doc.selection.getFirstPosition() );
+			} );
+
+			return waitForDebounce()
+				.then( () => {
+					sinon.assert.calledOnce( pinSpy );
+
+					model.change( writer => {
+						writer.insertText( 't', doc.selection.getFirstPosition() );
+					} );
+				} ).then( () => {
+					sinon.assert.calledTwice( pinSpy );
+				} );
+		} );
+	} );
+
 	describe( 'typing integration', () => {
 		describe( 'static list with default trigger', () => {
 			beforeEach( () => {
@@ -522,15 +617,26 @@ describe( 'MentionUI', () => {
 		} );
 	}
 
-	// TODO copied
 	function fireKeyDownEvent( options ) {
 		const eventInfo = new EventInfo( editingView.document, 'keydown' );
 		const eventData = new DomEventData( editingView.document, {
 			target: document.body
 		}, options );
 
-		// sinon.stub( eventInfo, 'stop' ).callsFake( evtStopSpy );
-
 		editingView.document.fire( eventInfo, eventData );
 	}
+
+	function stubSelectionRects( rects ) {
+		const originalViewRangeToDom = editingView.domConverter.viewRangeToDom;
+
+		// Mock selection rect.
+		sinon.stub( editingView.domConverter, 'viewRangeToDom' ).callsFake( ( ...args ) => {
+			const domRange = originalViewRangeToDom.apply( editingView.domConverter, args );
+
+			sinon.stub( domRange, 'getClientRects' )
+				.returns( rects );
+
+			return domRange;
+		} );
+	}
 } );