-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
inputobserver.ts
256 lines (219 loc) · 9.95 KB
/
inputobserver.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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/**
* @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 engine/view/observer/inputobserver
*/
import DomEventObserver from './domeventobserver.js';
import type DomEventData from './domeventdata.js';
import type ViewRange from '../range.js';
import DataTransfer from '../datatransfer.js';
import { env } from '@ckeditor/ckeditor5-utils';
/**
* Observer for events connected with data input.
*
* **Note**: This observer is attached by {@link module:engine/view/view~View} and available by default in all
* editor instances.
*/
export default class InputObserver extends DomEventObserver<'beforeinput'> {
/**
* @inheritDoc
*/
public readonly domEventType = 'beforeinput' as const;
/**
* @inheritDoc
*/
public onDomEvent( domEvent: InputEvent ): void {
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.group( `%c[InputObserver]%c ${ domEvent.type }: ${ domEvent.inputType }`,
// @if CK_DEBUG_TYPING // 'color: green', 'color: default'
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }
const domTargetRanges = domEvent.getTargetRanges();
const view = this.view;
const viewDocument = view.document;
let dataTransfer: DataTransfer | null = null;
let data: string | null = null;
let targetRanges: Array<ViewRange> = [];
if ( domEvent.dataTransfer ) {
dataTransfer = new DataTransfer( domEvent.dataTransfer );
}
if ( domEvent.data !== null ) {
data = domEvent.data;
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.info( `%c[InputObserver]%c event data: %c${ JSON.stringify( data ) }`,
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', 'color: blue;'
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }
} else if ( dataTransfer ) {
data = dataTransfer.getData( 'text/plain' );
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.info( `%c[InputObserver]%c event data transfer: %c${ JSON.stringify( data ) }`,
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', 'color: blue;'
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }
}
// If the editor selection is fake (an object is selected), the DOM range does not make sense because it is anchored
// in the fake selection container.
if ( viewDocument.selection.isFake ) {
// Future-proof: in case of multi-range fake selections being possible.
targetRanges = Array.from( viewDocument.selection.getRanges() );
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using fake selection:',
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges,
// @if CK_DEBUG_TYPING // viewDocument.selection.isFake ? 'fake view selection' : 'fake DOM parent'
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }
} else if ( domTargetRanges.length ) {
targetRanges = domTargetRanges.map( domRange => {
// Sometimes browser provides range that starts before editable node.
// We try to fall back to collapsed range at the valid end position.
// See https://github.com/ckeditor/ckeditor5/issues/14411.
// See https://github.com/ckeditor/ckeditor5/issues/14050.
const viewStart = view.domConverter.domPositionToView( domRange.startContainer, domRange.startOffset );
const viewEnd = view.domConverter.domPositionToView( domRange.endContainer, domRange.endOffset );
if ( viewStart ) {
return view.createRange( viewStart, viewEnd );
} else if ( viewEnd ) {
return view.createRange( viewEnd );
}
} ).filter( ( range ): range is ViewRange => !!range );
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using target ranges:',
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }
}
// For Android devices we use a fallback to the current DOM selection, Android modifies it according
// to the expected target ranges of input event.
else if ( env.isAndroid ) {
const domSelection = ( domEvent.target as HTMLElement ).ownerDocument.defaultView!.getSelection()!;
targetRanges = Array.from( view.domConverter.domSelectionToView( domSelection ).getRanges() );
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.info( '%c[InputObserver]%c using selection ranges:',
// @if CK_DEBUG_TYPING // 'color: green;font-weight: bold', 'font-weight:bold', targetRanges
// @if CK_DEBUG_TYPING // );
// @if CK_DEBUG_TYPING // }
}
// Android sometimes fires insertCompositionText with a new-line character at the end of the data
// instead of firing insertParagraph beforeInput event.
// Fire the correct type of beforeInput event and ignore the replaced fragment of text because
// it wants to replace "test" with "test\n".
// https://github.com/ckeditor/ckeditor5/issues/12368.
if ( env.isAndroid && domEvent.inputType == 'insertCompositionText' && data && data.endsWith( '\n' ) ) {
this.fire( domEvent.type, domEvent, {
inputType: 'insertParagraph',
targetRanges: [ view.createRange( targetRanges[ 0 ].end ) ]
} );
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.groupEnd();
// @if CK_DEBUG_TYPING // }
return;
}
// Normalize the insertText data that includes new-line characters.
// https://github.com/ckeditor/ckeditor5/issues/2045.
if ( domEvent.inputType == 'insertText' && data && data.includes( '\n' ) ) {
// There might be a single new-line or double for new paragraph, but we translate
// it to paragraphs as it is our default action for enter handling.
const parts = data.split( /\n{1,2}/g );
let partTargetRanges = targetRanges;
for ( let i = 0; i < parts.length; i++ ) {
const dataPart = parts[ i ];
if ( dataPart != '' ) {
this.fire( domEvent.type, domEvent, {
data: dataPart,
dataTransfer,
targetRanges: partTargetRanges,
inputType: domEvent.inputType,
isComposing: domEvent.isComposing
} );
// Use the result view selection so following events will be added one after another.
partTargetRanges = [ viewDocument.selection.getFirstRange()! ];
}
if ( i + 1 < parts.length ) {
this.fire( domEvent.type, domEvent, {
inputType: 'insertParagraph',
targetRanges: partTargetRanges
} );
// Use the result view selection so following events will be added one after another.
partTargetRanges = [ viewDocument.selection.getFirstRange()! ];
}
}
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.groupEnd();
// @if CK_DEBUG_TYPING // }
return;
}
// Fire the normalized beforeInput event.
this.fire( domEvent.type, domEvent, {
data,
dataTransfer,
targetRanges,
inputType: domEvent.inputType,
isComposing: domEvent.isComposing
} );
// @if CK_DEBUG_TYPING // if ( ( window as any ).logCKETyping ) {
// @if CK_DEBUG_TYPING // console.groupEnd();
// @if CK_DEBUG_TYPING // }
}
}
/**
* Fired before the web browser inputs, deletes, or formats some data.
*
* This event is introduced by {@link module:engine/view/observer/inputobserver~InputObserver} and available
* by default in all editor instances (attached by {@link module:engine/view/view~View}).
*
* @see module:engine/view/observer/inputobserver~InputObserver
* @eventName module:engine/view/document~Document#beforeinput
* @param data Event data containing detailed information about the event.
*/
export type ViewDocumentInputEvent = {
name: 'beforeinput';
args: [ data: InputEventData ];
};
/**
* The value of the {@link ~ViewDocumentInputEvent} event.
*/
export interface InputEventData extends DomEventData<InputEvent> {
/**
* The type of the input event (e.g. "insertText" or "deleteWordBackward"). Corresponds to native `InputEvent#inputType`.
*/
readonly inputType: string;
/**
* A unified text data passed along with the input event. Depending on:
*
* * the web browser and input events implementation (for instance [Level 1](https://www.w3.org/TR/input-events-1/) or
* [Level 2](https://www.w3.org/TR/input-events-2/)),
* * {@link module:engine/view/observer/inputobserver~InputEventData#inputType input type}
*
* text data is sometimes passed in the `data` and sometimes in the `dataTransfer` property.
*
* * If `InputEvent#data` was set, this property reflects its value.
* * If `InputEvent#data` is unavailable, this property contains the `'text/plain'` data from
* {@link module:engine/view/observer/inputobserver~InputEventData#dataTransfer}.
* * If the event ({@link module:engine/view/observer/inputobserver~InputEventData#inputType input type})
* provides no data whatsoever, this property is `null`.
*/
readonly data: string | null;
/**
* The data transfer instance of the input event. Corresponds to native `InputEvent#dataTransfer`.
*
* The value is `null` when no `dataTransfer` was passed along with the input event.
*/
readonly dataTransfer: DataTransfer;
/**
* A flag indicating that the `beforeinput` event was fired during composition.
*
* Corresponds to the
* {@link module:engine/view/document~Document#event:compositionstart},
* {@link module:engine/view/document~Document#event:compositionupdate},
* and {@link module:engine/view/document~Document#event:compositionend } trio.
*/
readonly isComposing: boolean;
/**
* Editing {@link module:engine/view/range~Range view ranges} corresponding to DOM ranges provided by the web browser
* (as returned by `InputEvent#getTargetRanges()`).
*/
readonly targetRanges: Array<ViewRange>;
}