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

Make panel attached to mention marker in editing area.

Maciej Gołaszewski 6 лет назад
Родитель
Сommit
116883d5dd

+ 41 - 16
packages/ckeditor5-mention/src/mentionui.js

@@ -91,7 +91,7 @@ export default class MentionUI extends Plugin {
 				}
 
 				if ( data.keyCode == keyCodes.esc ) {
-					this._hidePanel();
+					this._hidePanelAndRemoveMarker();
 				}
 			}
 		}, { priority: 'highest' } ); // priority highest required for enter overriding.
@@ -101,7 +101,7 @@ export default class MentionUI extends Plugin {
 			emitter: this.panelView,
 			contextElements: [ this.panelView.element ],
 			activator: () => this.panelView.isVisible,
-			callback: () => this._hidePanel()
+			callback: () => this._hidePanelAndRemoveMarker()
 		} );
 
 		const feeds = this.editor.config.get( 'mention.feeds' );
@@ -211,7 +211,7 @@ export default class MentionUI extends Plugin {
 				range
 			} );
 
-			this._hidePanel();
+			this._hidePanelAndRemoveMarker();
 		} );
 
 		return mentionsView;
@@ -271,6 +271,26 @@ export default class MentionUI extends Plugin {
 
 			const { feedText, marker } = matched;
 
+			const matchedTextLength = marker.length + feedText.length;
+
+			// create marker range
+			const start = selection.focus.getShiftedBy( -matchedTextLength );
+			const end = selection.focus.getShiftedBy( -feedText.length );
+
+			const markerRange = editor.model.createRange( start, end );
+
+			let markerMarker;
+
+			if ( editor.model.markers.has( 'mention' ) ) {
+				markerMarker = editor.model.markers.get( 'mention' );
+			} else {
+				markerMarker = editor.model.change( writer => writer.addMarker( 'mention', {
+					range: markerRange,
+					usingOperation: false,
+					affectsData: false
+				} ) );
+			}
+
 			this._getFeed( marker, feedText )
 				.then( feed => {
 					this._items.clear();
@@ -282,15 +302,15 @@ export default class MentionUI extends Plugin {
 					}
 
 					if ( this._items.length ) {
-						this._showPanel();
+						this._showPanel( markerMarker );
 					} else {
-						this._hidePanel();
+						this._hidePanelAndRemoveMarker();
 					}
 				} );
 		} );
 
 		watcher.on( 'unmatched', () => {
-			this._hidePanel();
+			this._hidePanelAndRemoveMarker();
 		} );
 
 		return watcher;
@@ -314,18 +334,22 @@ export default class MentionUI extends Plugin {
 	 *
 	 * @private
 	 */
-	_showPanel() {
-		this.panelView.pin( this._getBalloonPanelPositionData() );
+	_showPanel( markerMarker ) {
+		this.panelView.pin( this._getBalloonPanelPositionData( markerMarker ) );
 		this.panelView.show();
 		this._mentionsView.selectFirst();
 	}
 
 	/**
-	 * Hides the {@link #panelView}.
+	 * Hides the {@link #panelView} and remove 'mention' marker from markers collection.
 	 *
 	 * @private
 	 */
-	_hidePanel() {
+	_hidePanelAndRemoveMarker() {
+		if ( this.editor.model.markers.has( 'mention' ) ) {
+			this.editor.model.change( writer => writer.removeMarker( 'mention' ) );
+		}
+
 		this.panelView.unpin();
 		this.panelView.hide();
 	}
@@ -365,15 +389,16 @@ export default class MentionUI extends Plugin {
 	 * @returns {module:utils/dom/position~Options}
 	 * @private
 	 */
-	_getBalloonPanelPositionData() {
-		const view = this.editor.editing.view;
-		const domConverter = view.domConverter;
-		const viewSelection = view.document.selection;
+	_getBalloonPanelPositionData( markerMarker ) {
+		const editing = this.editor.editing;
+		const domConverter = editing.view.domConverter;
+		const mapper = editing.mapper;
 
 		return {
 			target: () => {
-				const range = viewSelection.getLastRange();
-				const rangeRects = Rect.getDomRangeRects( domConverter.viewRangeToDom( range ) );
+				const viewRange = mapper.toViewRange( markerMarker.getRange() );
+
+				const rangeRects = Rect.getDomRangeRects( domConverter.viewRangeToDom( viewRange ) );
 
 				return rangeRects.pop();
 			},

+ 45 - 17
packages/ckeditor5-mention/src/textwatcher.js

@@ -48,6 +48,15 @@ export default class TextWatcher {
 	_startListening() {
 		const editor = this.editor;
 
+		editor.model.document.selection.on( 'change', ( evt, { directChange } ) => {
+			// The indirect changes (ie on typing) are handled in document's change event.
+			if ( !directChange ) {
+				return;
+			}
+
+			this._evaluateTextBeforeSelection();
+		} );
+
 		editor.model.document.on( 'change', ( evt, batch ) => {
 			if ( batch.type == 'transparent' ) {
 				return;
@@ -58,29 +67,49 @@ export default class TextWatcher {
 
 			// Typing is represented by only a single change.
 			const isTypingChange = changes.length == 1 && entry.name == '$text' && entry.length == 1;
-			// Selection is represented by empty changes.
-			const isSelectionChange = changes.length == 0;
 
-			if ( !isTypingChange && !isSelectionChange ) {
+			if ( !isTypingChange ) {
 				return;
 			}
 
-			const text = this._getText();
-
-			const textHasMatch = this.testCallback( text );
+			this._evaluateTextBeforeSelection();
+		} );
+	}
 
-			if ( !textHasMatch && this.hasMatch ) {
-				this.fire( 'unmatched' );
-			}
+	/**
+	 * Checks the editor content for matched text.
+	 *
+	 * @fires matched
+	 * @fires unmatched
+	 *
+	 * @private
+	 */
+	_evaluateTextBeforeSelection() {
+		const text = this._getText();
+
+		const textHasMatch = this.testCallback( text );
+
+		if ( !textHasMatch && this.hasMatch ) {
+			/**
+			 * Fired whenever text doesn't match anymore. Fired only when text matcher was matched.
+			 *
+			 * @event unmatched
+			 */
+			this.fire( 'unmatched' );
+		}
 
-			this.hasMatch = textHasMatch;
+		this.hasMatch = textHasMatch;
 
-			if ( textHasMatch ) {
-				const matched = this.textMatcher( text );
+		if ( textHasMatch ) {
+			const matched = this.textMatcher( text );
 
-				this.fire( 'matched', { text, matched } );
-			}
-		} );
+			/**
+			 * Fired whenever text matcher was matched.
+			 *
+			 * @event matched
+			 */
+			this.fire( 'matched', { text, matched } );
+		}
 	}
 
 	/**
@@ -105,8 +134,7 @@ export default class TextWatcher {
 }
 
 // Returns whole text from parent element by adding all data from text nodes together.
-// @todo copied from autoformat...
-
+//
 // @private
 // @param {module:engine/model/element~Element} element
 // @returns {String}

+ 47 - 1
packages/ckeditor5-mention/tests/mentionui.js

@@ -111,6 +111,8 @@ describe( 'MentionUI', () => {
 			setData( model, '<paragraph>foo []</paragraph>' );
 			stubSelectionRects( [ caretRect ] );
 
+			expect( editor.model.markers.has( 'mention' ) ).to.be.false;
+
 			model.change( writer => {
 				writer.insertText( '@', doc.selection.getFirstPosition() );
 			} );
@@ -120,9 +122,25 @@ describe( 'MentionUI', () => {
 					const pinArgument = pinSpy.firstCall.args[ 0 ];
 					const { target, positions } = pinArgument;
 
-					expect( target() ).to.deep.equal( caretRect );
 					expect( positions ).to.have.length( 4 );
 
+					expect( editor.model.markers.has( 'mention' ) ).to.be.true;
+					const mentionMarker = editor.model.markers.get( 'mention' );
+					const focus = doc.selection.focus;
+					const expectedRange = editor.model.createRange( focus.getShiftedBy( -1 ), focus );
+
+					// It should create a model marker for matcher marker character ('@').
+					expect( expectedRange.isEqual( mentionMarker.getRange() ) ).to.be.true;
+
+					const toViewRangeSpy = sinon.spy( editor.editing.mapper, 'toViewRange' );
+
+					expect( target() ).to.deep.equal( caretRect );
+
+					sinon.assert.calledOnce( toViewRangeSpy );
+					const range = toViewRangeSpy.firstCall.args[ 0 ];
+
+					expect( mentionMarker.getRange().isEqual( range ), 'Should position to mention marker.' );
+
 					const caretSouthEast = positions[ 0 ];
 					const caretNorthEast = positions[ 1 ];
 					const caretSouthWest = positions[ 2 ];
@@ -195,6 +213,7 @@ describe( 'MentionUI', () => {
 				.then( waitForDebounce )
 				.then( () => {
 					expect( panelView.isVisible ).to.be.false;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 				} )
 				.then( waitForDebounce )
 				.then( () => {
@@ -205,6 +224,7 @@ describe( 'MentionUI', () => {
 				.then( waitForDebounce )
 				.then( () => {
 					expect( panelView.isVisible ).to.be.true;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 					expect( listView.items ).to.have.length( 1 );
 
 					model.change( writer => {
@@ -214,6 +234,7 @@ describe( 'MentionUI', () => {
 				.then( waitForDebounce )
 				.then( () => {
 					expect( panelView.isVisible ).to.be.true;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 					expect( listView.items ).to.have.length( 1 );
 				} );
 		} );
@@ -226,6 +247,8 @@ describe( 'MentionUI', () => {
 			it( 'should show panel for matched marker', () => {
 				setData( model, '<paragraph>foo []</paragraph>' );
 
+				expect( editor.model.markers.has( 'mention' ) ).to.be.false;
+
 				model.change( writer => {
 					writer.insertText( '@', doc.selection.getFirstPosition() );
 				} );
@@ -233,6 +256,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 						expect( listView.items ).to.have.length( 5 );
 					} );
 			} );
@@ -247,6 +271,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 						expect( listView.items ).to.have.length( 5 );
 					} );
 			} );
@@ -261,6 +286,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 					} );
 			} );
 
@@ -274,6 +300,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 					} );
 			} );
 
@@ -287,6 +314,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 					} );
 			} );
 
@@ -300,6 +328,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 
 						model.change( () => {
 							model.modifySelection( doc.selection, { direction: 'backward', unit: 'character' } );
@@ -308,6 +337,7 @@ describe( 'MentionUI', () => {
 					.then( waitForDebounce )
 					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 					} );
 			} );
 
@@ -325,6 +355,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 						expect( listView.items ).to.have.length( 1 );
 					} );
 			} );
@@ -361,6 +392,7 @@ describe( 'MentionUI', () => {
 					.then( waitForDebounce )
 					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 						expect( listView.items ).to.have.length( 0 );
 					} );
 			} );
@@ -417,6 +449,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 						expect( listView.items ).to.have.length( 4 );
 					} );
 			} );
@@ -435,6 +468,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 						expect( listView.items ).to.have.length( 1 );
 					} );
 			} );
@@ -456,6 +490,7 @@ describe( 'MentionUI', () => {
 					.then( waitForDebounce )
 					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 						expect( listView.items ).to.have.length( 0 );
 					} );
 			} );
@@ -496,6 +531,7 @@ describe( 'MentionUI', () => {
 				.then( waitForDebounce )
 				.then( () => {
 					expect( panelView.isVisible ).to.be.true;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 
 					fireKeyDownEvent( {
 						keyCode: keyCodes.esc,
@@ -504,6 +540,7 @@ describe( 'MentionUI', () => {
 					} );
 
 					expect( panelView.isVisible ).to.be.false;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 				} );
 		} );
 
@@ -519,10 +556,12 @@ describe( 'MentionUI', () => {
 				.then( waitForDebounce )
 				.then( () => {
 					expect( panelView.isVisible ).to.be.true;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 
 					document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
 
 					expect( panelView.isVisible ).to.be.false;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 				} );
 		} );
 
@@ -538,6 +577,7 @@ describe( 'MentionUI', () => {
 				.then( waitForDebounce )
 				.then( () => {
 					expect( panelView.isVisible ).to.be.true;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 
 					model.change( writer => {
 						// Place position at the begging of a paragraph.
@@ -545,6 +585,7 @@ describe( 'MentionUI', () => {
 					} );
 
 					expect( panelView.isVisible ).to.be.false;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 				} );
 		} );
 
@@ -673,6 +714,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 						expect( listView.items ).to.have.length( 5 );
 					} );
 			} );
@@ -847,6 +889,7 @@ describe( 'MentionUI', () => {
 				return waitForDebounce()
 					.then( () => {
 						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
 
 						fireKeyDownEvent( {
 							keyCode: keyCodes.esc,
@@ -855,6 +898,7 @@ describe( 'MentionUI', () => {
 						} );
 
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 
 						fireKeyDownEvent( {
 							keyCode,
@@ -865,6 +909,7 @@ describe( 'MentionUI', () => {
 						sinon.assert.notCalled( spy );
 
 						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 					} );
 			} );
 		}
@@ -912,6 +957,7 @@ describe( 'MentionUI', () => {
 					listView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
 
 					expect( panelView.isVisible ).to.be.false;
+					expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 				} );
 		} );
 	} );