瀏覽代碼

Merge branch 'master' into t/38

# Conflicts:
#	src/mentionui.js
Maciej Gołaszewski 6 年之前
父節點
當前提交
7a3adcfa5d

+ 1 - 1
packages/ckeditor5-mention/docs/features/mentions.md

@@ -189,7 +189,7 @@ To a link:
 
 The converters must be defined with a `'high'` priority to be executed before the {@link features/link link} feature's converter and before the default converter of the mention feature. A mention is stored in the model as a {@link framework/guides/architecture/editing-engine#text-attributes text attribute} that stores an object (see {@link module:mention/mention~MentionFeedItem}).
 
-**Note:** The feature prevents copying fragments of existing mentions. If only a part of a mention is selected, it will be copied as plain text. The internal converter with the {@link module:engine/conversion/conversion~ConverterDefinition#converterPriority `'highest'` priority} controls this behaviour; thus, we do not recommend adding mention converters with the `'highest'` priority to avoid collisions and quirky results. 
+**Note:** The feature prevents copying fragments of existing mentions. If only a part of a mention is selected, it will be copied as plain text. The internal converter with the {@link module:engine/conversion/conversion~ConverterDefinition `'highest'` priority} controls this behaviour; thus, we do not recommend adding mention converters with the `'highest'` priority to avoid collisions and quirky results.
 
 ```js
 ClassicEditor

+ 1 - 0
packages/ckeditor5-mention/package.json

@@ -12,6 +12,7 @@
   "dependencies": {
     "@ckeditor/ckeditor5-core": "^12.1.1",
     "@ckeditor/ckeditor5-ui": "^13.0.0",
+    "@ckeditor/ckeditor5-typing": "^12.0.2",
     "@ckeditor/ckeditor5-utils": "^12.1.1"
   },
   "devDependencies": {

+ 9 - 37
packages/ckeditor5-mention/src/mentionui.js

@@ -17,7 +17,7 @@ import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 
-import TextWatcher from './textwatcher';
+import TextWatcher from '@ckeditor/ckeditor5-typing/src/textwatcher';
 
 import MentionsView from './ui/mentionsview';
 import DomWrapperView from './ui/domwrapperview';
@@ -213,17 +213,11 @@ export default class MentionUI extends Plugin {
 			const item = data.item;
 			const marker = data.marker;
 
-			const watcher = this._getWatcher( marker );
-
-			const text = watcher.last;
-
-			const textMatcher = createTextMatcher( marker );
-			const matched = textMatcher( text );
-			const matchedTextLength = matched.marker.length + matched.feedText.length;
+			const mentionMarker = editor.model.markers.get( 'mention' );
 
 			// Create a range on matched text.
 			const end = model.createPositionAt( model.document.selection.focus );
-			const start = end.getShiftedBy( -matchedTextLength );
+			const start = model.createPositionAt( mentionMarker.getStart() );
 			const range = model.createRange( start, end );
 
 			this._hideUIAndRemoveMarker();
@@ -274,18 +268,15 @@ export default class MentionUI extends Plugin {
 	 * @private
 	 * @param {String} marker
 	 * @param {Number} minimumCharacters
-	 * @returns {module:mention/textwatcher~TextWatcher}
+	 * @returns {module:typing/textwatcher~TextWatcher}
 	 */
 	_setupTextWatcherForFeed( marker, minimumCharacters ) {
 		const editor = this.editor;
 
-		const watcher = new TextWatcher( editor, createTestCallback( marker, minimumCharacters ), createTextMatcher( marker ) );
+		const watcher = new TextWatcher( editor.model, createTestCallback( marker, minimumCharacters ) );
 
 		watcher.on( 'matched', ( evt, data ) => {
-			const matched = data.matched;
-
 			const selection = editor.model.document.selection;
-
 			const focus = selection.focus;
 
 			// The text watcher listens only to changed range in selection - so the selection attributes are not yet available
@@ -301,8 +292,7 @@ export default class MentionUI extends Plugin {
 				return;
 			}
 
-			const { feedText, marker } = matched;
-
+			const feedText = getFeedText( marker, data.text );
 			const matchedTextLength = marker.length + feedText.length;
 
 			// Create a marker range.
@@ -349,19 +339,6 @@ export default class MentionUI extends Plugin {
 	}
 
 	/**
-	 * Returns the registered text watcher for the marker.
-	 *
-	 * @private
-	 * @param {String} marker
-	 * @returns {module:mention/textwatcher~TextWatcher}
-	 */
-	_getWatcher( marker ) {
-		const { watcher } = this._mentionsConfigurations.get( marker );
-
-		return watcher;
-	}
-
-	/**
 	 * Shows the mentions balloon. If the panel is already visible, it will reposition it.
 	 *
 	 * @private
@@ -586,17 +563,12 @@ function createTestCallback( marker, minimumCharacters ) {
 //
 // @param {String} marker
 // @returns {Function}
-function createTextMatcher( marker ) {
+function getFeedText( marker, text ) {
 	const regExp = createRegExp( marker, 0 );
 
-	return text => {
-		const match = text.match( regExp );
+	const match = text.match( regExp );
 
-		const marker = match[ 1 ];
-		const feedText = match[ 2 ];
-
-		return { marker, feedText };
-	};
+	return match[ 3 ];
 }
 
 // The default feed callback.

+ 0 - 150
packages/ckeditor5-mention/src/textwatcher.js

@@ -1,150 +0,0 @@
-/**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
-
-/**
- * @module mention/textwatcher
- */
-
-import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-
-/**
- * The 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.
- *
- * @private
- */
-export default class TextWatcher {
-	/**
-	 * Creates a text watcher instance.
-	 * @param {module:core/editor/editor~Editor} editor
-	 * @param {Function} testCallback The function used to match the text.
-	 * @param {Function} textMatcherCallback The function used to process matched text.
-	 */
-	constructor( editor, testCallback, textMatcherCallback ) {
-		this.editor = editor;
-		this.testCallback = testCallback;
-		this.textMatcher = textMatcherCallback;
-
-		this.hasMatch = false;
-
-		this._startListening();
-	}
-
-	/**
-	 * The last matched text.
-	 *
-	 * @property {String}
-	 */
-	get last() {
-		return this._getText();
-	}
-
-	/**
-	 * Starts listening to the editor for typing and selection events.
-	 *
-	 * @private
-	 */
-	_startListening() {
-		const editor = this.editor;
-
-		editor.model.document.selection.on( 'change:range', ( evt, { directChange } ) => {
-			// Indirect changes (i.e. when the user types or external changes are applied) are handled in the document's change event.
-			if ( !directChange ) {
-				return;
-			}
-
-			this._evaluateTextBeforeSelection();
-		} );
-
-		editor.model.document.on( 'change:data', ( evt, batch ) => {
-			if ( batch.type == 'transparent' ) {
-				return false;
-			}
-
-			this._evaluateTextBeforeSelection();
-		} );
-	}
-
-	/**
-	 * 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 the text does not match anymore. Fired only when the text watcher found a match.
-			 *
-			 * @event unmatched
-			 */
-			this.fire( 'unmatched' );
-		}
-
-		this.hasMatch = textHasMatch;
-
-		if ( textHasMatch ) {
-			const matched = this.textMatcher( text );
-
-			/**
-			 * Fired whenever the text watcher found a match.
-			 *
-			 * @event matched
-			 */
-			this.fire( 'matched', { text, matched } );
-		}
-	}
-
-	/**
-	 * Returns the text before the caret from the current selection block.
-	 *
-	 * @returns {String|undefined} The text from the block or undefined if the selection is not collapsed.
-	 * @private
-	 */
-	_getText() {
-		const editor = this.editor;
-		const model = editor.model;
-		const selection = model.document.selection;
-
-		// Do nothing if the selection is not collapsed.
-		if ( !selection.isCollapsed ) {
-			return;
-		}
-
-		const rangeBeforeSelection = model.createRange( model.createPositionAt( selection.focus.parent, 0 ), selection.focus );
-
-		return _getText( rangeBeforeSelection );
-	}
-}
-
-/**
- * Returns the whole text from a given range by adding all data from the text nodes together.
- *
- * @protected
- * @param {module:engine/model/range~Range} range
- * @returns {String}
- */
-export function _getText( range ) {
-	return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
-		if ( node.is( 'softBreak' ) ) {
-			// Trim text to softBreak
-			return '';
-		}
-
-		return rangeText + node.data;
-	}, '' );
-}
-
-mix( TextWatcher, EmitterMixin );
-

+ 36 - 12
packages/ckeditor5-mention/tests/mentionui.js

@@ -585,34 +585,54 @@ describe( 'MentionUI', () => {
 			} );
 
 			it( 'should not show panel when selection is inside a mention', () => {
-				setData( model, '<paragraph>foo [@Lily] bar</paragraph>' );
-				model.change( writer => {
-					writer.setAttribute( 'mention', { id: '@Lily', _uid: 1234 }, doc.selection.getFirstRange() );
-				} );
+				setData( model, '<paragraph>foo @Lily bar[]</paragraph>' );
 
 				model.change( writer => {
-					writer.setSelection( doc.getRoot().getChild( 0 ), 7 );
+					const range = writer.createRange(
+						writer.createPositionAt( doc.getRoot().getChild( 0 ), 4 ),
+						writer.createPositionAt( doc.getRoot().getChild( 0 ), 9 )
+					);
+
+					writer.setAttribute( 'mention', { id: '@Lily', _uid: 1234 }, range );
 				} );
 
 				return waitForDebounce()
 					.then( () => {
+						model.change( writer => {
+							writer.setSelection( doc.getRoot().getChild( 0 ), 0 );
+						} );
+
+						expect( panelView.isVisible ).to.be.false;
+
+						model.change( writer => {
+							writer.setSelection( doc.getRoot().getChild( 0 ), 7 );
+						} );
+					} )
+					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
 						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 					} );
 			} );
 
 			it( 'should not show panel when selection is at the end of a mention', () => {
-				setData( model, '<paragraph>foo [@Lily] bar</paragraph>' );
-				model.change( writer => {
-					writer.setAttribute( 'mention', { id: '@Lily', _uid: 1234 }, doc.selection.getFirstRange() );
-				} );
+				setData( model, '<paragraph>foo @Lily bar[]</paragraph>' );
 
 				model.change( writer => {
-					writer.setSelection( doc.getRoot().getChild( 0 ), 9 );
+					const range = writer.createRange(
+						writer.createPositionAt( doc.getRoot().getChild( 0 ), 4 ),
+						writer.createPositionAt( doc.getRoot().getChild( 0 ), 9 )
+					);
+
+					writer.setAttribute( 'mention', { id: '@Lily', _uid: 1234 }, range );
 				} );
 
 				return waitForDebounce()
 					.then( () => {
+						model.change( writer => {
+							writer.setSelection( doc.getRoot().getChild( 0 ), 9 );
+						} );
+					} )
+					.then( () => {
 						expect( panelView.isVisible ).to.be.false;
 						expect( editor.model.markers.has( 'mention' ) ).to.be.false;
 					} );
@@ -695,10 +715,14 @@ describe( 'MentionUI', () => {
 			} );
 
 			it( 'should not show panel when selection moves inside existing mention', () => {
-				setData( model, '<paragraph>foo [@Lily] bar</paragraph>' );
+				setData( model, '<paragraph>foo @Lily bar[]</paragraph>' );
 
 				model.change( writer => {
-					writer.setAttribute( 'mention', { id: '@Lily', _uid: 1234 }, doc.selection.getFirstRange() );
+					const range = writer.createRange(
+						writer.createPositionAt( doc.getRoot().getChild( 0 ), 4 ),
+						writer.createPositionAt( doc.getRoot().getChild( 0 ), 9 )
+					);
+					writer.setAttribute( 'mention', { id: '@Lily', _uid: 1234 }, range );
 				} );
 
 				return waitForDebounce()