@ckeditor/ckeditor5-typing
Version:
Typing feature for CKEditor 5.
113 lines (112 loc) • 5.47 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
/**
* @module typing/inserttextobserver
*/
import { env, EventInfo } from '@ckeditor/ckeditor5-utils';
import { DomEventData, Observer, FocusObserver } from '@ckeditor/ckeditor5-engine';
// @if CK_DEBUG_TYPING // const { _buildLogMessage } = require( '@ckeditor/ckeditor5-engine/src/dev-utils/utils.js' );
const TYPING_INPUT_TYPES = [
// For collapsed range:
// - This one is a regular typing (all browsers, all systems).
// - This one is used by Chrome when typing accented letter – 2nd step when the user selects the accent (Mac).
// For non-collapsed range:
// - This one is used by Chrome when typing accented letter – when the selection box first appears (Mac).
// - This one is used by Safari when accepting spell check suggestions from the context menu (Mac).
'insertText',
// This one is used by Safari when typing accented letter (Mac).
// This one is used by Safari when accepting spell check suggestions from the autocorrection pop-up (Mac).
'insertReplacementText'
];
const TYPING_INPUT_TYPES_ANDROID = [
...TYPING_INPUT_TYPES,
'insertCompositionText'
];
/**
* Text insertion observer introduces the {@link module:engine/view/document~Document#event:insertText} event.
*/
export default class InsertTextObserver extends Observer {
/**
* Instance of the focus observer. Insert text observer calls
* {@link module:engine/view/observer/focusobserver~FocusObserver#flush} to mark the latest focus change as complete.
*/
focusObserver;
/**
* @inheritDoc
*/
constructor(view) {
super(view);
this.focusObserver = view.getObserver(FocusObserver);
// On Android composition events should immediately be applied to the model. Rendering is not disabled.
// On non-Android the model is updated only on composition end.
// On Android we can't rely on composition start/end to update model.
const typingInputTypes = env.isAndroid ? TYPING_INPUT_TYPES_ANDROID : TYPING_INPUT_TYPES;
const viewDocument = view.document;
viewDocument.on('beforeinput', (evt, data) => {
if (!this.isEnabled) {
return;
}
const { data: text, targetRanges, inputType, domEvent, isComposing } = data;
if (!typingInputTypes.includes(inputType)) {
return;
}
// Mark the latest focus change as complete (we are typing in editable after the focus
// so the selection is in the focused element).
this.focusObserver.flush();
const eventInfo = new EventInfo(viewDocument, 'insertText');
viewDocument.fire(eventInfo, new DomEventData(view, domEvent, {
text,
selection: view.createSelection(targetRanges),
isComposing
}));
// Stop the beforeinput event if `delete` event was stopped.
// https://github.com/ckeditor/ckeditor5/issues/753
if (eventInfo.stop.called) {
evt.stop();
}
});
// On Android composition events are immediately applied to the model.
// On non-Android the model is updated only on composition end.
// On Android we can't rely on composition start/end to update model.
if (!env.isAndroid) {
// Note: The priority must be lower than the CompositionObserver handler to call it after the renderer is unblocked.
// This is important for view to DOM position mapping.
// This causes the effect of first remove composed DOM and then reapply it after model modification.
viewDocument.on('compositionend', (evt, { data, domEvent }) => {
if (!this.isEnabled) {
return;
}
// In case of aborted composition.
if (!data) {
return;
}
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.log( ..._buildLogMessage( this, 'InsertTextObserver',
// @if CK_DEBUG_TYPING // `%cFire insertText event, %c${ JSON.stringify( data ) }`,
// @if CK_DEBUG_TYPING // 'font-weight: bold',
// @if CK_DEBUG_TYPING // 'color: blue'
// @if CK_DEBUG_TYPING // ) );
// @if CK_DEBUG_TYPING // }
// How do we know where to insert the composed text?
// 1. The SelectionObserver is blocked and the view is not updated with the composition changes.
// 2. The last moment before it's locked is the `compositionstart` event.
// 3. The `SelectionObserver` is listening for `compositionstart` event and immediately converts
// the selection. Handle this at the low priority so after the rendering is blocked.
viewDocument.fire('insertText', new DomEventData(view, domEvent, {
text: data,
isComposing: true
}));
}, { priority: 'low' });
}
}
/**
* @inheritDoc
*/
observe() { }
/**
* @inheritDoc
*/
stopObserving() { }
}