|
@@ -70,9 +70,17 @@ export default class MentionUI extends Plugin {
|
|
|
*/
|
|
*/
|
|
|
this._mentionsConfigurations = new Map();
|
|
this._mentionsConfigurations = new Map();
|
|
|
|
|
|
|
|
- editor.config.define( 'mention', { feeds: [] } );
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Debounced feed requester. It uses `lodash#debounce` method to delay function call.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @private
|
|
|
|
|
+ * @param {String} marker
|
|
|
|
|
+ * @param {String} feedText
|
|
|
|
|
+ * @method #_requestFeedDebounced
|
|
|
|
|
+ */
|
|
|
|
|
+ this._requestFeedDebounced = debounce( this._requestFeed, 100 );
|
|
|
|
|
|
|
|
- this._getFeedDebounced = debounce( this._getFeed, 100 );
|
|
|
|
|
|
|
+ editor.config.define( 'mention', { feeds: [] } );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -156,8 +164,8 @@ export default class MentionUI extends Plugin {
|
|
|
this._mentionsConfigurations.set( marker, definition );
|
|
this._mentionsConfigurations.set( marker, definition );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.on( 'getFeed:response', ( evt, data ) => this._handleFeedResponse( data ) );
|
|
|
|
|
- this.on( 'getFeed:error', () => this._hideUIAndRemoveMarker() );
|
|
|
|
|
|
|
+ this.on( 'requestFeed:response', ( evt, data ) => this._handleFeedResponse( data ) );
|
|
|
|
|
+ this.on( 'requestFeed:error', () => this._hideUIAndRemoveMarker() );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -260,40 +268,42 @@ export default class MentionUI extends Plugin {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Returns a promise that resolves with autocomplete items for a given text.
|
|
|
|
|
- *
|
|
|
|
|
- * **Note**: This method unifies the responses from feed callbacks to always return a promise.
|
|
|
|
|
|
|
+ * Requests a feed from a configured callbacks.
|
|
|
*
|
|
*
|
|
|
* @param {String} marker
|
|
* @param {String} marker
|
|
|
* @param {String} feedText
|
|
* @param {String} feedText
|
|
|
* @private
|
|
* @private
|
|
|
*/
|
|
*/
|
|
|
- _getFeed( marker, feedText ) {
|
|
|
|
|
- const { feedCallback } = this._mentionsConfigurations.get( marker );
|
|
|
|
|
-
|
|
|
|
|
|
|
+ _requestFeed( marker, feedText ) {
|
|
|
|
|
+ // Store the last requested feed - it is used to discard any out-of order requests.
|
|
|
this._lastRequested = feedText;
|
|
this._lastRequested = feedText;
|
|
|
|
|
|
|
|
- const response = feedCallback( feedText );
|
|
|
|
|
|
|
+ const { feedCallback } = this._mentionsConfigurations.get( marker );
|
|
|
|
|
+ const feedResponse = feedCallback( feedText );
|
|
|
|
|
|
|
|
- const isAsynchronous = response instanceof Promise;
|
|
|
|
|
|
|
+ const isAsynchronous = feedResponse instanceof Promise;
|
|
|
|
|
|
|
|
if ( !isAsynchronous ) {
|
|
if ( !isAsynchronous ) {
|
|
|
- this.fire( 'getFeed:response', { response, marker, feedText } );
|
|
|
|
|
|
|
+ // For synchronous feeds (e.g. callbacks, arrays) fire the response event immediately.
|
|
|
|
|
+ this.fire( 'requestFeed:response', { feed: feedResponse, marker, feedText } );
|
|
|
|
|
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- response
|
|
|
|
|
|
|
+ // Handle the asynchronous responses.
|
|
|
|
|
+ feedResponse
|
|
|
.then( response => {
|
|
.then( response => {
|
|
|
|
|
+ // Check the feed text of this response with the last requested one so either:
|
|
|
if ( this._lastRequested == feedText ) {
|
|
if ( this._lastRequested == feedText ) {
|
|
|
- this.fire( 'getFeed:response', { response, marker, feedText } );
|
|
|
|
|
|
|
+ // It is the same and fire the response event.
|
|
|
|
|
+ this.fire( 'requestFeed:response', { feed: response, marker, feedText } );
|
|
|
} else {
|
|
} else {
|
|
|
- // Discard a response that is not for the last requested feed.
|
|
|
|
|
- this.fire( 'getFeed:discarded', { response, marker, feedText } );
|
|
|
|
|
|
|
+ // It is different - most probably out-of-order one, so fire the discarded event.
|
|
|
|
|
+ this.fire( 'requestFeed:discarded', { feed: response, marker, feedText } );
|
|
|
}
|
|
}
|
|
|
} )
|
|
} )
|
|
|
.catch( error => {
|
|
.catch( error => {
|
|
|
- this.fire( 'getFeed:error', { error } );
|
|
|
|
|
|
|
+ this.fire( 'requestFeed:error', { error } );
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* The callback used for obtaining mention autocomplete feed thrown and error and the mention UI was hidden or
|
|
* The callback used for obtaining mention autocomplete feed thrown and error and the mention UI was hidden or
|
|
@@ -305,35 +315,6 @@ export default class MentionUI extends Plugin {
|
|
|
} );
|
|
} );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- _handleFeedResponse( data ) {
|
|
|
|
|
- const { response: feed, marker } = data;
|
|
|
|
|
-
|
|
|
|
|
- // Do nothing if :
|
|
|
|
|
- // - feed was discarded or empty feed was passed.
|
|
|
|
|
- // - if the marker is not in the document - the selection might have already changed.
|
|
|
|
|
- if ( !feed || !this.editor.model.markers.has( 'mention' ) ) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Remove old entries.
|
|
|
|
|
- this._items.clear();
|
|
|
|
|
-
|
|
|
|
|
- for ( const feedItem of feed ) {
|
|
|
|
|
- const item = typeof feedItem != 'object' ? { id: feedItem, text: feedItem } : feedItem;
|
|
|
|
|
-
|
|
|
|
|
- this._items.add( { item, marker } );
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const markerMarker = this.editor.model.markers.get( 'mention' );
|
|
|
|
|
-
|
|
|
|
|
- if ( this._items.length && markerMarker ) {
|
|
|
|
|
- this._showUI( markerMarker );
|
|
|
|
|
- } else {
|
|
|
|
|
- // Do not show empty mention UI.
|
|
|
|
|
- this._hideUIAndRemoveMarker();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* Registers a text watcher for the marker.
|
|
* Registers a text watcher for the marker.
|
|
|
*
|
|
*
|
|
@@ -351,20 +332,13 @@ export default class MentionUI extends Plugin {
|
|
|
const selection = editor.model.document.selection;
|
|
const selection = editor.model.document.selection;
|
|
|
const focus = selection.focus;
|
|
const focus = selection.focus;
|
|
|
|
|
|
|
|
- // The text watcher listens only to changed range in selection - so the selection attributes are not yet available
|
|
|
|
|
- // and you cannot use selection.hasAttribute( 'mention' ) just yet.
|
|
|
|
|
- // See https://github.com/ckeditor/ckeditor5-engine/issues/1723.
|
|
|
|
|
- const hasMention = focus.textNode && focus.textNode.hasAttribute( 'mention' );
|
|
|
|
|
-
|
|
|
|
|
- const nodeBefore = focus.nodeBefore;
|
|
|
|
|
-
|
|
|
|
|
- if ( hasMention || nodeBefore && nodeBefore.is( 'text' ) && nodeBefore.hasAttribute( 'mention' ) ) {
|
|
|
|
|
|
|
+ if ( hasExistingMention( focus ) ) {
|
|
|
this._hideUIAndRemoveMarker();
|
|
this._hideUIAndRemoveMarker();
|
|
|
|
|
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const feedText = getFeedText( marker, data.text );
|
|
|
|
|
|
|
+ const feedText = requestFeedText( marker, data.text );
|
|
|
const matchedTextLength = marker.length + feedText.length;
|
|
const matchedTextLength = marker.length + feedText.length;
|
|
|
|
|
|
|
|
// Create a marker range.
|
|
// Create a marker range.
|
|
@@ -374,12 +348,12 @@ export default class MentionUI extends Plugin {
|
|
|
const markerRange = editor.model.createRange( start, end );
|
|
const markerRange = editor.model.createRange( start, end );
|
|
|
|
|
|
|
|
if ( editor.model.markers.has( 'mention' ) ) {
|
|
if ( editor.model.markers.has( 'mention' ) ) {
|
|
|
- const currentMentionMarker = editor.model.markers.get( 'mention' );
|
|
|
|
|
|
|
+ const mentionMarker = editor.model.markers.get( 'mention' );
|
|
|
|
|
|
|
|
// TODO - there's no tests for this
|
|
// TODO - there's no tests for this
|
|
|
// Update the marker - user might've moved the selection to other mention trigger.
|
|
// Update the marker - user might've moved the selection to other mention trigger.
|
|
|
editor.model.change( writer => {
|
|
editor.model.change( writer => {
|
|
|
- writer.updateMarker( currentMentionMarker, { range: markerRange } );
|
|
|
|
|
|
|
+ writer.updateMarker( mentionMarker, { range: markerRange } );
|
|
|
} );
|
|
} );
|
|
|
} else {
|
|
} else {
|
|
|
editor.model.change( writer => {
|
|
editor.model.change( writer => {
|
|
@@ -387,7 +361,7 @@ export default class MentionUI extends Plugin {
|
|
|
} );
|
|
} );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this._getFeedDebounced( marker, feedText );
|
|
|
|
|
|
|
+ this._requestFeedDebounced( marker, feedText );
|
|
|
} );
|
|
} );
|
|
|
|
|
|
|
|
watcher.on( 'unmatched', () => {
|
|
watcher.on( 'unmatched', () => {
|
|
@@ -397,12 +371,45 @@ export default class MentionUI extends Plugin {
|
|
|
return watcher;
|
|
return watcher;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Handles the feed response event data.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param data
|
|
|
|
|
+ * @private
|
|
|
|
|
+ */
|
|
|
|
|
+ _handleFeedResponse( data ) {
|
|
|
|
|
+ const { feed, marker } = data;
|
|
|
|
|
+
|
|
|
|
|
+ // If the marker is not in the document happens when the selection had changed and the 'mention' marker was removed.
|
|
|
|
|
+ if ( !this.editor.model.markers.has( 'mention' ) ) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Reset the view.
|
|
|
|
|
+ this._items.clear();
|
|
|
|
|
+
|
|
|
|
|
+ for ( const feedItem of feed ) {
|
|
|
|
|
+ const item = typeof feedItem != 'object' ? { id: feedItem, text: feedItem } : feedItem;
|
|
|
|
|
+
|
|
|
|
|
+ this._items.add( { item, marker } );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const mentionMarker = this.editor.model.markers.get( 'mention' );
|
|
|
|
|
+
|
|
|
|
|
+ if ( this._items.length ) {
|
|
|
|
|
+ this._showOrUpdateUI( mentionMarker );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Do not show empty mention UI.
|
|
|
|
|
+ this._hideUIAndRemoveMarker();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Shows the mentions balloon. If the panel is already visible, it will reposition it.
|
|
* Shows the mentions balloon. If the panel is already visible, it will reposition it.
|
|
|
*
|
|
*
|
|
|
* @private
|
|
* @private
|
|
|
*/
|
|
*/
|
|
|
- _showUI( markerMarker ) {
|
|
|
|
|
|
|
+ _showOrUpdateUI( markerMarker ) {
|
|
|
if ( this._isUIVisible ) {
|
|
if ( this._isUIVisible ) {
|
|
|
// Update balloon position as the mention list view may change its size.
|
|
// Update balloon position as the mention list view may change its size.
|
|
|
this._balloon.updatePosition( this._getBalloonPanelPositionData( markerMarker, this._mentionsView.position ) );
|
|
this._balloon.updatePosition( this._getBalloonPanelPositionData( markerMarker, this._mentionsView.position ) );
|
|
@@ -621,7 +628,7 @@ function createTestCallback( marker, minimumCharacters ) {
|
|
|
//
|
|
//
|
|
|
// @param {String} marker
|
|
// @param {String} marker
|
|
|
// @returns {Function}
|
|
// @returns {Function}
|
|
|
-function getFeedText( marker, text ) {
|
|
|
|
|
|
|
+function requestFeedText( marker, text ) {
|
|
|
const regExp = createRegExp( marker, 0 );
|
|
const regExp = createRegExp( marker, 0 );
|
|
|
|
|
|
|
|
const match = text.match( regExp );
|
|
const match = text.match( regExp );
|
|
@@ -664,3 +671,18 @@ function isHandledKey( keyCode ) {
|
|
|
|
|
|
|
|
return handledKeyCodes.includes( keyCode );
|
|
return handledKeyCodes.includes( keyCode );
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// Checks if position in inside or right after a text with a mention.
|
|
|
|
|
+//
|
|
|
|
|
+// @param {module:engine/model/position~Position} position.
|
|
|
|
|
+// @returns {Boolean}
|
|
|
|
|
+function hasExistingMention( position ) {
|
|
|
|
|
+ // The text watcher listens only to changed range in selection - so the selection attributes are not yet available
|
|
|
|
|
+ // and you cannot use selection.hasAttribute( 'mention' ) just yet.
|
|
|
|
|
+ // See https://github.com/ckeditor/ckeditor5-engine/issues/1723.
|
|
|
|
|
+ const hasMention = position.textNode && position.textNode.hasAttribute( 'mention' );
|
|
|
|
|
+
|
|
|
|
|
+ const nodeBefore = position.nodeBefore;
|
|
|
|
|
+
|
|
|
|
|
+ return hasMention || nodeBefore && nodeBefore.is( 'text' ) && nodeBefore.hasAttribute( 'mention' );
|
|
|
|
|
+}
|