@ckeditor/ckeditor5-typing 35.2.1 โ†’ 35.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,128 +2,221 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module typing/deleteobserver
8
7
  */
9
-
10
8
  import Observer from '@ckeditor/ckeditor5-engine/src/view/observer/observer';
11
9
  import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
12
10
  import BubblingEventInfo from '@ckeditor/ckeditor5-engine/src/view/observer/bubblingeventinfo';
13
- import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
14
- import env from '@ckeditor/ckeditor5-utils/src/env';
15
- import { isShiftDeleteOnNonCollapsedSelection } from './utils/utils';
16
-
11
+ import { env, keyCodes } from '@ckeditor/ckeditor5-utils';
12
+ const DELETE_CHARACTER = 'character';
13
+ const DELETE_WORD = 'word';
14
+ const DELETE_CODE_POINT = 'codePoint';
15
+ const DELETE_SELECTION = 'selection';
16
+ const DELETE_BACKWARD = 'backward';
17
+ const DELETE_FORWARD = 'forward';
18
+ const DELETE_EVENT_TYPES = {
19
+ // --------------------------------------- Backward delete types -----------------------------------------------------
20
+ // This happens in Safari on Mac when some content is selected and Ctrl + K is pressed.
21
+ deleteContent: {
22
+ unit: DELETE_SELECTION,
23
+ // According to the Input Events Level 2 spec, this delete type has no direction
24
+ // but to keep things simple, let's default to backward.
25
+ direction: DELETE_BACKWARD
26
+ },
27
+ // Chrome and Safari on Mac: Backspace or Ctrl + H
28
+ deleteContentBackward: {
29
+ // This kind of deletions must be done on the code point-level instead of target range provided by the DOM beforeinput event.
30
+ // Take for instance "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", it equals:
31
+ //
32
+ // * [ "๐Ÿ‘จ", "ZERO WIDTH JOINER", "๐Ÿ‘ฉ", "ZERO WIDTH JOINER", "๐Ÿ‘ง", "ZERO WIDTH JOINER", "๐Ÿ‘ง" ]
33
+ // * or simply "\u{1F468}\u200D\u{1F469}\u200D\u{1F467}\u200D\u{1F467}"
34
+ //
35
+ // The range provided by the browser would cause the entire multi-byte grapheme to disappear while the user
36
+ // intention when deleting backwards ("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง[]", then backspace) is gradual "decomposition" (first to "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€[]",
37
+ // then to "๐Ÿ‘จโ€๐Ÿ‘ฉโ€[]", etc.).
38
+ //
39
+ // * "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง[]" + backward delete (by code point) -> results in "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง[]", removed the last "๐Ÿ‘ง" ๐Ÿ‘
40
+ // * "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง[]" + backward delete (by character) -> results in "[]", removed the whole grapheme ๐Ÿ‘Ž
41
+ //
42
+ // Deleting by code-point is simply a better UX. See "deleteContentForward" to learn more.
43
+ unit: DELETE_CODE_POINT,
44
+ direction: DELETE_BACKWARD
45
+ },
46
+ // On Mac: Option + Backspace.
47
+ // On iOS: Hold the backspace for a while and the whole words will start to disappear.
48
+ deleteWordBackward: {
49
+ unit: DELETE_WORD,
50
+ direction: DELETE_BACKWARD
51
+ },
52
+ // Safari on Mac: Cmd + Backspace
53
+ deleteHardLineBackward: {
54
+ unit: DELETE_SELECTION,
55
+ direction: DELETE_BACKWARD
56
+ },
57
+ // Chrome on Mac: Cmd + Backspace.
58
+ deleteSoftLineBackward: {
59
+ unit: DELETE_SELECTION,
60
+ direction: DELETE_BACKWARD
61
+ },
62
+ // --------------------------------------- Forward delete types -----------------------------------------------------
63
+ // Chrome on Mac: Fn + Backspace or Ctrl + D
64
+ // Safari on Mac: Ctrl + K or Ctrl + D
65
+ deleteContentForward: {
66
+ // Unlike backward delete, this delete must be performed by character instead of by code point, which
67
+ // provides the best UX for working with accented letters.
68
+ // Take, for example "bฬ‚" ("\u0062\u0302", or [ "LATIN SMALL LETTER B", "COMBINING CIRCUMFLEX ACCENT" ]):
69
+ //
70
+ // * "bฬ‚[]" + backward delete (by code point) -> results in "b[]", removed the combining mark ๐Ÿ‘
71
+ // * "[]bฬ‚" + forward delete (by code point) -> results in "[]^", a bare combining mark does that not make sense when alone ๐Ÿ‘Ž
72
+ // * "[]bฬ‚" + forward delete (by character) -> results in "[]", removed both "b" and the combining mark ๐Ÿ‘
73
+ //
74
+ // See: "deleteContentBackward" to learn more.
75
+ unit: DELETE_CHARACTER,
76
+ direction: DELETE_FORWARD
77
+ },
78
+ // On Mac: Fn + Option + Backspace.
79
+ deleteWordForward: {
80
+ unit: DELETE_WORD,
81
+ direction: DELETE_FORWARD
82
+ },
83
+ // Chrome on Mac: Ctrl + K (you have to disable the Link plugin first, though, because it uses the same keystroke)
84
+ // This is weird that it does not work in Safari on Mac despite being listed in the official shortcuts listing
85
+ // on Apple's webpage.
86
+ deleteHardLineForward: {
87
+ unit: DELETE_SELECTION,
88
+ direction: DELETE_FORWARD
89
+ },
90
+ // At this moment there is no known way to trigger this event type but let's keep it for the symmetry with
91
+ // deleteSoftLineBackward.
92
+ deleteSoftLineForward: {
93
+ unit: DELETE_SELECTION,
94
+ direction: DELETE_FORWARD
95
+ }
96
+ };
17
97
  /**
18
98
  * Delete observer introduces the {@link module:engine/view/document~Document#event:delete} event.
19
99
  *
20
100
  * @extends module:engine/view/observer/observer~Observer
21
101
  */
22
102
  export default class DeleteObserver extends Observer {
23
- /**
24
- * @inheritDoc
25
- */
26
- constructor( view ) {
27
- super( view );
28
-
29
- const document = view.document;
30
- let sequence = 0;
31
-
32
- document.on( 'keyup', ( evt, data ) => {
33
- if ( data.keyCode == keyCodes.delete || data.keyCode == keyCodes.backspace ) {
34
- sequence = 0;
35
- }
36
- } );
37
-
38
- document.on( 'keydown', ( evt, data ) => {
39
- // Do not fire the `delete` event, if Shift + Delete key combination was pressed on a non-collapsed selection on Windows.
40
- //
41
- // The Shift + Delete key combination should work in the same way as the `cut` event on a non-collapsed selection on Windows.
42
- // In fact, the native `cut` event is actually emitted in this case, but with lower priority. Therefore, in order to handle the
43
- // Shift + Delete key combination correctly, it is enough not to emit the `delete` event.
44
- if ( env.isWindows && isShiftDeleteOnNonCollapsedSelection( data, document ) ) {
45
- return;
46
- }
47
-
48
- const deleteData = {};
49
-
50
- if ( data.keyCode == keyCodes.delete ) {
51
- deleteData.direction = 'forward';
52
- deleteData.unit = 'character';
53
- } else if ( data.keyCode == keyCodes.backspace ) {
54
- deleteData.direction = 'backward';
55
- deleteData.unit = 'codePoint';
56
- } else {
57
- return;
58
- }
59
-
60
- const hasWordModifier = env.isMac ? data.altKey : data.ctrlKey;
61
- deleteData.unit = hasWordModifier ? 'word' : deleteData.unit;
62
- deleteData.sequence = ++sequence;
63
-
64
- fireViewDeleteEvent( evt, data.domEvent, deleteData );
65
- } );
66
-
67
- // `beforeinput` is handled only for Android devices. Desktop Chrome and iOS are skipped because they are working fine now.
68
- if ( env.isAndroid ) {
69
- document.on( 'beforeinput', ( evt, data ) => {
70
- // If event type is other than `deleteContentBackward` then this is not deleting.
71
- if ( data.domEvent.inputType != 'deleteContentBackward' ) {
72
- return;
73
- }
74
-
75
- const deleteData = {
76
- unit: 'codepoint',
77
- direction: 'backward',
78
- sequence: 1
79
- };
80
-
81
- // Android IMEs may change the DOM selection on `beforeinput` event so that the selection contains all the text
82
- // that the IME wants to remove. We will pass this information to `delete` event so proper part of the content is removed.
83
- //
84
- // Sometimes it is only expanding by a one character (in case of collapsed selection). In this case we don't need to
85
- // set a different selection to remove, it will work just fine.
86
- const domSelection = data.domTarget.ownerDocument.defaultView.getSelection();
87
-
88
- if ( domSelection.anchorNode == domSelection.focusNode && domSelection.anchorOffset + 1 != domSelection.focusOffset ) {
89
- deleteData.selectionToRemove = view.domConverter.domSelectionToView( domSelection );
90
- }
91
-
92
- fireViewDeleteEvent( evt, data.domEvent, deleteData );
93
- } );
94
- }
95
-
96
- function fireViewDeleteEvent( originalEvent, domEvent, deleteData ) {
97
- const event = new BubblingEventInfo( document, 'delete', document.selection.getFirstRange() );
98
-
99
- document.fire( event, new DomEventData( document, domEvent, deleteData ) );
100
-
101
- // Stop the original event if `delete` event was stopped.
102
- // https://github.com/ckeditor/ckeditor5/issues/753
103
- if ( event.stop.called ) {
104
- originalEvent.stop();
105
- }
106
- }
107
- }
108
-
109
- /**
110
- * @inheritDoc
111
- */
112
- observe() {}
103
+ /**
104
+ * @inheritDoc
105
+ */
106
+ constructor(view) {
107
+ super(view);
108
+ const document = view.document;
109
+ // It matters how many subsequent deletions were made, e.g. when the backspace key was pressed and held
110
+ // by the user for some time. For instance, if such scenario ocurred and the heading the selection was
111
+ // anchored to was the only content of the editor, it will not be converted into a paragraph (the user
112
+ // wanted to clean it up, not remove it, it's about UX). Check out the DeleteCommand implementation to learn more.
113
+ //
114
+ // Fun fact: Safari on Mac won't fire beforeinput for backspace in an empty heading (only content).
115
+ let sequence = 0;
116
+ document.on('keydown', () => {
117
+ sequence++;
118
+ });
119
+ document.on('keyup', () => {
120
+ sequence = 0;
121
+ });
122
+ document.on('beforeinput', (evt, data) => {
123
+ if (!this.isEnabled) {
124
+ return;
125
+ }
126
+ const { targetRanges, domEvent, inputType } = data;
127
+ const deleteEventSpec = DELETE_EVENT_TYPES[inputType];
128
+ if (!deleteEventSpec) {
129
+ return;
130
+ }
131
+ const deleteData = {
132
+ direction: deleteEventSpec.direction,
133
+ unit: deleteEventSpec.unit,
134
+ sequence
135
+ };
136
+ if (deleteData.unit == DELETE_SELECTION) {
137
+ deleteData.selectionToRemove = view.createSelection(targetRanges[0]);
138
+ }
139
+ // The default deletion unit for deleteContentBackward is a single code point
140
+ // but on Android it sometimes passes a wider target range, so we need to change
141
+ // the unit of deletion to include the whole range to be removed and not a single code point.
142
+ if (env.isAndroid && inputType === 'deleteContentBackward') {
143
+ // On Android, deleteContentBackward has sequence 1 by default.
144
+ deleteData.sequence = 1;
145
+ // IME wants more than a single character to be removed.
146
+ if (targetRanges.length == 1 && (targetRanges[0].start.parent != targetRanges[0].end.parent ||
147
+ targetRanges[0].start.offset + 1 != targetRanges[0].end.offset)) {
148
+ deleteData.unit = DELETE_SELECTION;
149
+ deleteData.selectionToRemove = view.createSelection(targetRanges);
150
+ }
151
+ }
152
+ const eventInfo = new BubblingEventInfo(document, 'delete', targetRanges[0]);
153
+ document.fire(eventInfo, new DomEventData(view, domEvent, deleteData));
154
+ // Stop the beforeinput event if `delete` event was stopped.
155
+ // https://github.com/ckeditor/ckeditor5/issues/753
156
+ if (eventInfo.stop.called) {
157
+ evt.stop();
158
+ }
159
+ });
160
+ // TODO: to be removed when https://bugs.chromium.org/p/chromium/issues/detail?id=1365311 is solved.
161
+ if (env.isBlink) {
162
+ enableChromeWorkaround(this);
163
+ }
164
+ }
165
+ /**
166
+ * @inheritDoc
167
+ */
168
+ observe() { }
169
+ }
170
+ // Enables workaround for the issue https://github.com/ckeditor/ckeditor5/issues/11904.
171
+ function enableChromeWorkaround(observer) {
172
+ const view = observer.view;
173
+ const document = view.document;
174
+ let pressedKeyCode = null;
175
+ let beforeInputReceived = false;
176
+ document.on('keydown', (evt, { keyCode }) => {
177
+ pressedKeyCode = keyCode;
178
+ beforeInputReceived = false;
179
+ });
180
+ document.on('keyup', (evt, { keyCode, domEvent }) => {
181
+ const selection = document.selection;
182
+ const shouldFireDeleteEvent = observer.isEnabled &&
183
+ keyCode == pressedKeyCode &&
184
+ isDeleteKeyCode(keyCode) &&
185
+ !selection.isCollapsed &&
186
+ !beforeInputReceived;
187
+ pressedKeyCode = null;
188
+ if (shouldFireDeleteEvent) {
189
+ const targetRange = selection.getFirstRange();
190
+ const eventInfo = new BubblingEventInfo(document, 'delete', targetRange);
191
+ const deleteData = {
192
+ unit: DELETE_SELECTION,
193
+ direction: getDeleteDirection(keyCode),
194
+ selectionToRemove: selection
195
+ };
196
+ document.fire(eventInfo, new DomEventData(view, domEvent, deleteData));
197
+ }
198
+ });
199
+ document.on('beforeinput', (evt, { inputType }) => {
200
+ const deleteEventSpec = DELETE_EVENT_TYPES[inputType];
201
+ const isMatchingBeforeInput = isDeleteKeyCode(pressedKeyCode) &&
202
+ deleteEventSpec &&
203
+ deleteEventSpec.direction == getDeleteDirection(pressedKeyCode);
204
+ if (isMatchingBeforeInput) {
205
+ beforeInputReceived = true;
206
+ }
207
+ });
208
+ document.on('beforeinput', (evt, { inputType, data }) => {
209
+ const shouldIgnoreBeforeInput = pressedKeyCode == keyCodes.delete &&
210
+ inputType == 'insertText' &&
211
+ data == '\x7f'; // Delete character :P
212
+ if (shouldIgnoreBeforeInput) {
213
+ evt.stop();
214
+ }
215
+ }, { priority: 'high' });
216
+ function isDeleteKeyCode(keyCode) {
217
+ return keyCode == keyCodes.backspace || keyCode == keyCodes.delete;
218
+ }
219
+ function getDeleteDirection(keyCode) {
220
+ return keyCode == keyCodes.backspace ? DELETE_BACKWARD : DELETE_FORWARD;
221
+ }
113
222
  }
114
-
115
- /**
116
- * Event fired when the user tries to delete content (e.g. presses <kbd>Delete</kbd> or <kbd>Backspace</kbd>).
117
- *
118
- * Note: This event is fired by the {@link module:typing/deleteobserver~DeleteObserver observer}
119
- * (usually registered by the {@link module:typing/delete~Delete delete feature}).
120
- *
121
- * @event module:engine/view/document~Document#event:delete
122
- * @param {module:engine/view/observer/domeventdata~DomEventData} data
123
- * @param {'forward'|'delete'} data.direction The direction in which the deletion should happen.
124
- * @param {'character'|'codePoint'|'word'} data.unit The "amount" of content that should be deleted.
125
- * @param {Number} data.sequence A number describing which subsequent delete event it is without the key being released.
126
- * If it's 2 or more it means that the key was pressed and hold.
127
- * @param {module:engine/view/selection~Selection} [data.selectionToRemove] View selection which content should be removed. If not set,
128
- * current selection should be used.
129
- */
package/src/index.js CHANGED
@@ -2,21 +2,15 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module typing
8
7
  */
9
-
10
8
  export { default as Typing } from './typing';
11
9
  export { default as Input } from './input';
12
10
  export { default as Delete } from './delete';
13
-
14
11
  export { default as TextWatcher } from './textwatcher';
15
12
  export { default as TwoStepCaretMovement } from './twostepcaretmovement';
16
13
  export { default as TextTransformation } from './texttransformation';
17
-
18
14
  export { default as inlineHighlight } from './utils/inlinehighlight';
19
15
  export { default as findAttributeRange } from './utils/findattributerange';
20
16
  export { default as getLastTextLine } from './utils/getlasttextline';
21
-
22
- export * from './utils/injectunsafekeystrokeshandling';
package/src/input.js CHANGED
@@ -2,42 +2,139 @@
2
2
  * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
-
6
5
  /**
7
6
  * @module typing/input
8
7
  */
9
-
10
8
  import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11
- import InputCommand from './inputcommand';
12
-
13
- import injectUnsafeKeystrokesHandling from './utils/injectunsafekeystrokeshandling';
14
- import injectTypingMutationsHandling from './utils/injecttypingmutationshandling';
15
-
9
+ import InsertTextCommand from './inserttextcommand';
10
+ import InsertTextObserver from './inserttextobserver';
11
+ import env from '@ckeditor/ckeditor5-utils/src/env';
12
+ // Import config.typing declaration.
13
+ import './typingconfig';
16
14
  /**
17
15
  * Handles text input coming from the keyboard or other input methods.
18
16
  *
19
17
  * @extends module:core/plugin~Plugin
20
18
  */
21
19
  export default class Input extends Plugin {
22
- /**
23
- * @inheritDoc
24
- */
25
- static get pluginName() {
26
- return 'Input';
27
- }
28
-
29
- /**
30
- * @inheritDoc
31
- */
32
- init() {
33
- const editor = this.editor;
34
-
35
- // TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
36
- const inputCommand = new InputCommand( editor, editor.config.get( 'typing.undoStep' ) || 20 );
37
-
38
- editor.commands.add( 'input', inputCommand );
39
-
40
- injectUnsafeKeystrokesHandling( editor );
41
- injectTypingMutationsHandling( editor );
42
- }
20
+ /**
21
+ * @inheritDoc
22
+ */
23
+ static get pluginName() {
24
+ return 'Input';
25
+ }
26
+ /**
27
+ * @inheritDoc
28
+ */
29
+ init() {
30
+ const editor = this.editor;
31
+ const model = editor.model;
32
+ const view = editor.editing.view;
33
+ const modelSelection = model.document.selection;
34
+ view.addObserver(InsertTextObserver);
35
+ // TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
36
+ const insertTextCommand = new InsertTextCommand(editor, editor.config.get('typing.undoStep') || 20);
37
+ // Register `insertText` command and add `input` command as an alias for backward compatibility.
38
+ editor.commands.add('insertText', insertTextCommand);
39
+ editor.commands.add('input', insertTextCommand);
40
+ this.listenTo(view.document, 'insertText', (evt, data) => {
41
+ // Rendering is disabled while composing so prevent events that will be rendered by the engine
42
+ // and should not be applied by the browser.
43
+ if (!view.document.isComposing) {
44
+ data.preventDefault();
45
+ }
46
+ const { text, selection: viewSelection, resultRange: viewResultRange } = data;
47
+ // If view selection was specified, translate it to model selection.
48
+ const modelRanges = Array.from(viewSelection.getRanges()).map(viewRange => {
49
+ return editor.editing.mapper.toModelRange(viewRange);
50
+ });
51
+ let insertText = text;
52
+ // Typing in English on Android is firing composition events for the whole typed word.
53
+ // We need to check the target range text to only apply the difference.
54
+ if (env.isAndroid) {
55
+ const selectedText = Array.from(modelRanges[0].getItems()).reduce((rangeText, node) => {
56
+ return rangeText + (node.is('$textProxy') ? node.data : '');
57
+ }, '');
58
+ if (selectedText) {
59
+ if (selectedText.length <= insertText.length) {
60
+ if (insertText.startsWith(selectedText)) {
61
+ insertText = insertText.substring(selectedText.length);
62
+ modelRanges[0].start = modelRanges[0].start.getShiftedBy(selectedText.length);
63
+ }
64
+ }
65
+ else {
66
+ if (selectedText.startsWith(insertText)) {
67
+ // TODO this should be mapped as delete?
68
+ modelRanges[0].start = modelRanges[0].start.getShiftedBy(insertText.length);
69
+ insertText = '';
70
+ }
71
+ }
72
+ }
73
+ }
74
+ const insertTextCommandData = {
75
+ text: insertText,
76
+ selection: model.createSelection(modelRanges)
77
+ };
78
+ // @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
79
+ // @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Execute insertText:',
80
+ // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', '',
81
+ // @if CK_DEBUG_TYPING // insertText,
82
+ // @if CK_DEBUG_TYPING // `[${ modelRanges[ 0 ].start.path }]-[${ modelRanges[ 0 ].end.path }]`
83
+ // @if CK_DEBUG_TYPING // );
84
+ // @if CK_DEBUG_TYPING // }
85
+ if (viewResultRange) {
86
+ insertTextCommandData.resultRange = editor.editing.mapper.toModelRange(viewResultRange);
87
+ }
88
+ editor.execute('insertText', insertTextCommandData);
89
+ });
90
+ if (env.isAndroid) {
91
+ // On Android with English keyboard, the composition starts just by putting caret
92
+ // at the word end or by selecting a table column. This is not a real composition started.
93
+ // Trigger delete content on first composition key pressed.
94
+ this.listenTo(view.document, 'keydown', (evt, data) => {
95
+ if (modelSelection.isCollapsed || data.keyCode != 229 || !view.document.isComposing) {
96
+ return;
97
+ }
98
+ // @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
99
+ // @if CK_DEBUG_TYPING // console.log( '%c[Input]%c KeyDown 229 -> model.deleteContent()',
100
+ // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', '',
101
+ // @if CK_DEBUG_TYPING // `[${ modelSelection.getFirstPosition().path }]-[${ modelSelection.getLastPosition().path }]`
102
+ // @if CK_DEBUG_TYPING // );
103
+ // @if CK_DEBUG_TYPING // }
104
+ deleteSelectionContent(model, insertTextCommand);
105
+ });
106
+ }
107
+ else {
108
+ // Note: The priority must precede the CompositionObserver handler to call it before
109
+ // the renderer is blocked, because we want to render this change.
110
+ this.listenTo(view.document, 'compositionstart', () => {
111
+ if (modelSelection.isCollapsed) {
112
+ return;
113
+ }
114
+ // @if CK_DEBUG_TYPING // if ( window.logCKETyping ) {
115
+ // @if CK_DEBUG_TYPING // console.log( '%c[Input]%c Composition start -> model.deleteContent()',
116
+ // @if CK_DEBUG_TYPING // 'font-weight: bold; color: green;', '',
117
+ // @if CK_DEBUG_TYPING // `[${ modelSelection.getFirstPosition().path }]-[${ modelSelection.getLastPosition().path }]`
118
+ // @if CK_DEBUG_TYPING // );
119
+ // @if CK_DEBUG_TYPING // }
120
+ deleteSelectionContent(model, insertTextCommand);
121
+ });
122
+ }
123
+ }
124
+ }
125
+ function deleteSelectionContent(model, insertTextCommand) {
126
+ // By relying on the state of the input command we allow disabling the entire input easily
127
+ // by just disabling the input command. We couldโ€™ve used here the delete command but that
128
+ // would mean requiring the delete feature which would block loading one without the other.
129
+ // We could also check the editor.isReadOnly property, but that wouldn't allow to block
130
+ // the input without blocking other features.
131
+ if (!insertTextCommand.isEnabled) {
132
+ return;
133
+ }
134
+ const buffer = insertTextCommand.buffer;
135
+ buffer.lock();
136
+ model.enqueueChange(buffer.batch, () => {
137
+ model.deleteContent(model.document.selection);
138
+ });
139
+ buffer.unlock();
43
140
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module typing/inserttextcommand
7
+ */
8
+ import Command from '@ckeditor/ckeditor5-core/src/command';
9
+ import ChangeBuffer from './utils/changebuffer';
10
+ /**
11
+ * The insert text command. Used by the {@link module:typing/input~Input input feature} to handle typing.
12
+ *
13
+ * @extends module:core/command~Command
14
+ */
15
+ export default class InsertTextCommand extends Command {
16
+ /**
17
+ * Creates an instance of the command.
18
+ *
19
+ * @param {module:core/editor/editor~Editor} editor
20
+ * @param {Number} undoStepSize The maximum number of atomic changes
21
+ * which can be contained in one batch in the command buffer.
22
+ */
23
+ constructor(editor, undoStepSize) {
24
+ super(editor);
25
+ /**
26
+ * Typing's change buffer used to group subsequent changes into batches.
27
+ *
28
+ * @readonly
29
+ * @private
30
+ * @member {module:typing/utils/changebuffer~ChangeBuffer} #_buffer
31
+ */
32
+ this._buffer = new ChangeBuffer(editor.model, undoStepSize);
33
+ }
34
+ /**
35
+ * The current change buffer.
36
+ *
37
+ * @type {module:typing/utils/changebuffer~ChangeBuffer}
38
+ */
39
+ get buffer() {
40
+ return this._buffer;
41
+ }
42
+ /**
43
+ * @inheritDoc
44
+ */
45
+ destroy() {
46
+ super.destroy();
47
+ this._buffer.destroy();
48
+ }
49
+ /**
50
+ * Executes the input command. It replaces the content within the given range with the given text.
51
+ * Replacing is a two step process, first the content within the range is removed and then the new text is inserted
52
+ * at the beginning of the range (which after the removal is a collapsed range).
53
+ *
54
+ * @fires execute
55
+ * @param {Object} [options] The command options.
56
+ * @param {String} [options.text=''] The text to be inserted.
57
+ * @param {module:engine/model/selection~Selection} [options.selection] The selection in which the text is inserted.
58
+ * Inserting a text into a selection deletes the current content within selection ranges. If the selection is not specified,
59
+ * the current selection in the model will be used instead.
60
+ * // TODO note that those 2 options are exclusive (either selection or range)
61
+ * @param {module:engine/model/range~Range} [options.range] The range in which the text is inserted. Defaults
62
+ * to the first range in the current selection.
63
+ * @param {module:engine/model/range~Range} [options.resultRange] The range where the selection
64
+ * should be placed after the insertion. If not specified, the selection will be placed right after
65
+ * the inserted text.
66
+ */
67
+ execute(options = {}) {
68
+ const model = this.editor.model;
69
+ const doc = model.document;
70
+ const text = options.text || '';
71
+ const textInsertions = text.length;
72
+ let selection = doc.selection;
73
+ if (options.selection) {
74
+ selection = options.selection;
75
+ }
76
+ else if (options.range) {
77
+ selection = model.createSelection(options.range);
78
+ }
79
+ const resultRange = options.resultRange;
80
+ model.enqueueChange(this._buffer.batch, writer => {
81
+ this._buffer.lock();
82
+ model.deleteContent(selection);
83
+ if (text) {
84
+ model.insertContent(writer.createText(text, doc.selection.getAttributes()), selection);
85
+ }
86
+ if (resultRange) {
87
+ writer.setSelection(resultRange);
88
+ }
89
+ else if (!selection.is('documentSelection')) {
90
+ writer.setSelection(selection);
91
+ }
92
+ this._buffer.unlock();
93
+ this._buffer.input(textInsertions);
94
+ });
95
+ }
96
+ }