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

Make "unmatched" event to be fired before any "matched" event for all create text watchers.

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

+ 1 - 1
packages/ckeditor5-mention/src/mentionui.js

@@ -518,7 +518,7 @@ function getBalloonPanelPositions( positionName ) {
 function createPattern( marker, minimumCharacters ) {
 	const numberOfCharacters = minimumCharacters == 0 ? '*' : `{${ minimumCharacters },}`;
 
-	return `(^| )(${ marker })([_a-zA-Z0-9À-ž]${ numberOfCharacters }?)$`;
+	return `(^| )(\\${ marker })([_a-zA-Z0-9À-ž]${ numberOfCharacters }?)$`;
 }
 
 // Creates a test callback for marker to be used in text watcher instance.

+ 68 - 15
packages/ckeditor5-mention/src/textwatcher.js

@@ -9,9 +9,20 @@
 
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
+
+// Fire all "unmatched" events before any "matched" events.
+const UNMATCH_EVENT_PRIORITY = priorities.normal + 10;
+const MATCH_EVENT_PRIORITY = priorities.normal;
 
 /**
  * Text watcher feature.
+ *
+ * Fires {@link module:mention/textwatcher~TextWatcher#event:matched matched} and
+ * {@link module:mention/textwatcher~TextWatcher#event:unmatched unmatched} events on typing or selection changes.
+ *
+ * **Note**: The "unmatched" events for any created text watchers are fired before any "matched" events of another text watchers.
+ *
  * @private
  */
 export default class TextWatcher {
@@ -48,45 +59,54 @@ export default class TextWatcher {
 	_startListening() {
 		const editor = this.editor;
 
+		// Register "unmatch" evaluator for selection changes.
 		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();
-		} );
+			this._evaluateTextBeforeSelectionForUnmatch();
+		}, { priority: UNMATCH_EVENT_PRIORITY } );
 
-		editor.model.document.on( 'change', ( evt, batch ) => {
-			if ( batch.type == 'transparent' ) {
+		// Register "match" evaluator for selection changes.
+		editor.model.document.selection.on( 'change', ( evt, { directChange } ) => {
+			// The indirect changes (ie on typing) are handled in document's change event.
+			if ( !directChange ) {
 				return;
 			}
 
-			const changes = Array.from( editor.model.document.differ.getChanges() );
-			const entry = changes[ 0 ];
+			this._evaluateTextBeforeSelectionForMatch();
+		}, { priority: MATCH_EVENT_PRIORITY } );
+
+		// Register "unmatch" evaluator for typing changes.
+		editor.model.document.on( 'change:data', ( evt, batch ) => {
+			if ( !this._isTypingChange( batch ) ) {
+				return;
+			}
 
-			// Typing is represented by only a single change.
-			const isTypingChange = changes.length == 1 && entry.name == '$text' && entry.length == 1;
+			this._evaluateTextBeforeSelectionForUnmatch();
+		}, { priority: UNMATCH_EVENT_PRIORITY } );
 
-			if ( !isTypingChange ) {
+		// Register "match" evaluator for typing changes.
+		editor.model.document.on( 'change:data', ( evt, batch ) => {
+			if ( !this._isTypingChange( batch ) ) {
 				return;
 			}
 
-			this._evaluateTextBeforeSelection();
-		} );
+			this._evaluateTextBeforeSelectionForMatch();
+		}, { priority: MATCH_EVENT_PRIORITY } );
 	}
 
 	/**
 	 * Checks the editor content for matched text.
 	 *
-	 * @fires matched
-	 * @fires unmatched
+	 * @fires module:mention/textwatcher~TextWatcher#unmatched
 	 *
 	 * @private
 	 */
-	_evaluateTextBeforeSelection() {
+	_evaluateTextBeforeSelectionForUnmatch() {
 		const text = this._getText();
-
 		const textHasMatch = this.testCallback( text );
 
 		if ( !textHasMatch && this.hasMatch ) {
@@ -97,6 +117,18 @@ export default class TextWatcher {
 			 */
 			this.fire( 'unmatched' );
 		}
+	}
+
+	/**
+	 * Checks the editor content for unmatched text.
+	 *
+	 * @fires module:mention/textwatcher~TextWatcher#matched
+	 *
+	 * @private
+	 */
+	_evaluateTextBeforeSelectionForMatch() {
+		const text = this._getText();
+		const textHasMatch = this.testCallback( text );
 
 		this.hasMatch = textHasMatch;
 
@@ -112,6 +144,27 @@ export default class TextWatcher {
 		}
 	}
 
+	/**
+	 * Returns true if batch contains typing change. Typing change is detected as single character insertion.
+	 *
+	 * @param {module:engine/model/batch~Batch} batch
+	 * @returns {Boolean}
+	 * @private
+	 */
+	_isTypingChange( batch ) {
+		const editor = this.editor;
+
+		if ( batch.type == 'transparent' ) {
+			return false;
+		}
+
+		const changes = Array.from( editor.model.document.differ.getChanges() );
+		const entry = changes[ 0 ];
+
+		// Typing is represented by only a single change.
+		return changes.length == 1 && entry.name == '$text' && entry.length == 1;
+	}
+
 	/**
 	 * Returns the text before the caret from the current selection block.
 	 *

+ 67 - 0
packages/ckeditor5-mention/tests/mentionui.js

@@ -1222,6 +1222,73 @@ describe( 'MentionUI', () => {
 			} );
 		} );
 
+		describe( 'multiple feeds configuration', () => {
+			beforeEach( () => {
+				return createClassicTestEditor( {
+					feeds: [
+						{
+							marker: '@',
+							feed: [ '@a1', '@a2', '@a3' ]
+						},
+						{
+							marker: '$',
+							feed: [ '$a1', '$a2', '$a3', '$a4', '$a5' ]
+						}
+					]
+				} );
+			} );
+
+			it( 'should show panel for matched marker', () => {
+				setData( model, '<paragraph>foo []</paragraph>' );
+
+				model.change( writer => {
+					writer.insertText( '@', doc.selection.getFirstPosition() );
+				} );
+
+				return waitForDebounce()
+					.then( () => {
+						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
+						expect( listView.items ).to.have.length( 3 );
+
+						listView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
+					} )
+					.then( waitForDebounce )
+					.then( () => {
+						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
+
+						model.change( writer => {
+							writer.insertText( '$', doc.selection.getFirstPosition() );
+						} );
+					} )
+					.then( waitForDebounce )
+					.then( () => {
+						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
+						expect( listView.items ).to.have.length( 5 );
+
+						listView.items.get( 0 ).children.get( 0 ).fire( 'execute' );
+					} )
+					.then( waitForDebounce )
+					.then( () => {
+						expect( panelView.isVisible ).to.be.false;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
+
+						model.change( writer => {
+							writer.insertText( '@', doc.selection.getFirstPosition() );
+						} );
+					} )
+					.then( waitForDebounce )
+					.then( () => {
+						expect( panelView.isVisible ).to.be.true;
+						expect( editor.model.markers.has( 'mention' ) ).to.be.true;
+
+						expect( listView.items ).to.have.length( 3 );
+					} );
+			} );
+		} );
+
 		function testExecuteKey( name, keyCode, feedItems ) {
 			it( 'should execute selected button on ' + name, () => {
 				setData( model, '<paragraph>foo []</paragraph>' );