/
inserttextobserver.ts
183 lines (155 loc) · 6.44 KB
/
inserttextobserver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module typing/inserttextobserver
*/
import { env, EventInfo } from '@ckeditor/ckeditor5-utils';
import {
DomEventData,
Observer,
FocusObserver,
type EditingView,
type ViewDocumentCompositionEndEvent,
type ViewDocumentInputEvent,
type ViewDocumentSelection,
type ViewRange,
type ViewSelection
} from '@ckeditor/ckeditor5-engine';
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'
];
/**
* 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.
*/
public readonly focusObserver: FocusObserver;
/**
* @inheritDoc
*/
constructor( view: EditingView ) {
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.
if ( env.isAndroid ) {
TYPING_INPUT_TYPES.push( 'insertCompositionText' );
}
const viewDocument = view.document;
viewDocument.on<ViewDocumentInputEvent>( 'beforeinput', ( evt, data ) => {
if ( !this.isEnabled ) {
return;
}
const { data: text, targetRanges, inputType, domEvent } = data;
if ( !TYPING_INPUT_TYPES.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 )
} ) );
// Stop the beforeinput event if `delete` event was stopped.
// https://github.com/ckeditor/ckeditor5/issues/753
if ( eventInfo.stop.called ) {
evt.stop();
}
} );
// Note: The priority must be lower than the CompositionObserver handler to call it after the renderer is unblocked.
viewDocument.on<ViewDocumentCompositionEndEvent>( 'compositionend', ( evt, { data, domEvent } ) => {
// 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 ( !this.isEnabled || env.isAndroid ) {
return;
}
// In case of aborted composition.
if ( !data ) {
return;
}
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.log( `%c[InsertTextObserver]%c Fire insertText event, text: ${ JSON.stringify( data ) }`,
// @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', ''
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }
// How do we know where to insert the composed text?
// The selection observer is blocked and the view is not updated with the composition changes.
// There were three options:
// - Store the selection on `compositionstart` and use it now. This wouldn't work in RTC
// where the view would change and the stored selection might get incorrect.
// We'd need to fallback to the current view selection anyway.
// - Use the current view selection. This is a bit weird and non-intuitive because
// this isn't necessarily the selection on which the user started composing.
// We cannot even know whether it's still collapsed (there might be some weird
// editor feature that changed it in unpredictable ways for us). But it's by far
// the simplest solution and should be stable (the selection is definitely correct)
// and probably mostly predictable (features usually don't modify the selection
// unless called explicitly by the user).
// - Try to follow it from the `beforeinput` events. This would be really complex as each
// `beforeinput` would come with just the range it's changing and we'd need to calculate that.
// We decided to go with the 2nd option for its simplicity and stability.
viewDocument.fire( 'insertText', new DomEventData( view, domEvent, {
text: data,
selection: viewDocument.selection
} ) );
}, { priority: 'lowest' } );
}
/**
* @inheritDoc
*/
public observe(): void {}
/**
* @inheritDoc
*/
public stopObserving(): void {}
}
/**
* Event fired when the user types text, for instance presses <kbd>A</kbd> or <kbd>?</kbd> in the
* editing view document.
*
* **Note**: This event will **not** fire for keystrokes such as <kbd>Delete</kbd> or <kbd>Enter</kbd>.
* They have dedicated events, see {@link module:engine/view/document~Document#event:delete} and
* {@link module:engine/view/document~Document#event:enter} to learn more.
*
* **Note**: This event is fired by the {@link module:typing/inserttextobserver~InsertTextObserver input feature}.
*
* @eventName module:engine/view/document~Document#insertText
* @param data The event data.
*/
export type ViewDocumentInsertTextEvent = {
name: 'insertText';
args: [ data: InsertTextEventData ];
};
export interface InsertTextEventData extends DomEventData {
/**
* The text to be inserted.
*/
text: string;
/**
* The selection into which the text should be inserted.
* If not specified, the insertion should occur at the current view selection.
*/
selection: ViewSelection | ViewDocumentSelection;
/**
* The range that view selection should be set to after insertion.
*/
resultRange?: ViewRange;
}