UNPKG

ckeditor5-image-upload-base64

Version:

The development environment of CKEditor 5 – the best browser-based rich text editor.

196 lines (167 loc) 5.6 kB
/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module typing/textwatcher */ import mix from '@ckeditor/ckeditor5-utils/src/mix'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import getLastTextLine from './utils/getlasttextline'; /** * The text watcher feature. * * Fires the {@link module:typing/textwatcher~TextWatcher#event:matched:data `matched:data`}, * {@link module:typing/textwatcher~TextWatcher#event:matched:selection `matched:selection`} and * {@link module:typing/textwatcher~TextWatcher#event:unmatched `unmatched`} events on typing or selection changes. * * @private * @mixes module:utils/observablemixin~ObservableMixin */ export default class TextWatcher { /** * Creates a text watcher instance. * * @param {module:engine/model/model~Model} model * @param {Function} testCallback See {@link module:typing/textwatcher~TextWatcher#testCallback}. */ constructor( model, testCallback ) { /** * The editor's model. * * @readonly * @member {module:engine/model/model~Model} */ this.model = model; /** * The function used to match the text. * * The test callback can return 3 values: * * * `false` if there is no match, * * `true` if there is a match, * * an object if there is a match and we want to pass some additional information to the {@link #event:matched:data} event. * * @member {Function} #testCallback * @returns {Object} testResult */ this.testCallback = testCallback; /** * Whether there is a match currently. * * @readonly * @member {Boolean} */ this.hasMatch = false; /** * Flag indicating whether the `TextWatcher` instance is enabled or disabled. * A disabled TextWatcher will not evaluate text. * * To disable TextWatcher: * * const watcher = new TextWatcher( editor.model, testCallback ); * * // After this a testCallback will not be called. * watcher.isEnabled = false; * * @observable * @member {Boolean} #isEnabled */ this.set( 'isEnabled', true ); // Toggle text watching on isEnabled state change. this.on( 'change:isEnabled', () => { if ( this.isEnabled ) { this._startListening(); } else { this.stopListening( model.document.selection ); this.stopListening( model.document ); } } ); this._startListening(); } /** * Starts listening to the editor for typing and selection events. * * @private */ _startListening() { const model = this.model; const document = model.document; this.listenTo( document.selection, '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; } // Act only on collapsed selection. if ( !document.selection.isCollapsed ) { if ( this.hasMatch ) { this.fire( 'unmatched' ); this.hasMatch = false; } return; } this._evaluateTextBeforeSelection( 'selection' ); } ); this.listenTo( document, 'change:data', ( evt, batch ) => { if ( batch.type == 'transparent' ) { return; } this._evaluateTextBeforeSelection( 'data', { batch } ); } ); } /** * Checks the editor content for matched text. * * @fires matched:data * @fires matched:selection * @fires unmatched * * @private * @param {'data'|'selection'} suffix A suffix used for generating the event name. * @param {Object} data Data object for event. */ _evaluateTextBeforeSelection( suffix, data = {} ) { const model = this.model; const document = model.document; const selection = document.selection; const rangeBeforeSelection = model.createRange( model.createPositionAt( selection.focus.parent, 0 ), selection.focus ); const { text, range } = getLastTextLine( rangeBeforeSelection, model ); const testResult = this.testCallback( text ); if ( !testResult && this.hasMatch ) { this.fire( 'unmatched' ); } this.hasMatch = !!testResult; if ( testResult ) { const eventData = Object.assign( data, { text, range } ); // If the test callback returns an object with additional data, assign the data as well. if ( typeof testResult == 'object' ) { Object.assign( eventData, testResult ); } this.fire( `matched:${ suffix }`, eventData ); } } } mix( TextWatcher, ObservableMixin ); /** * Fired whenever the text watcher found a match for data changes. * * @event matched:data * @param {Object} data Event data. * @param {String} data.text The full text before selection to which the regexp was applied. * @param {module:engine/model/range~Range} data.range The range representing the position of the `data.text`. * @param {Object} [data.testResult] The additional data returned from the {@link module:typing/textwatcher~TextWatcher#testCallback}. */ /** * Fired whenever the text watcher found a match for selection changes. * * @event matched:selection * @param {Object} data Event data. * @param {String} data.text The full text before selection. * @param {module:engine/model/range~Range} data.range The range representing the position of the `data.text`. * @param {Object} [data.testResult] The additional data returned from the {@link module:typing/textwatcher~TextWatcher#testCallback}. */ /** * Fired whenever the text does not match anymore. Fired only when the text watcher found a match. * * @event unmatched */