|
|
@@ -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.
|
|
|
*
|