ckeditor5-image-upload-base64
Version:
The development environment of CKEditor 5 – the best browser-based rich text editor.
181 lines (154 loc) • 6.41 kB
JavaScript
/**
* @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/utils/injectunsafekeystrokeshandling
*/
import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
import env from '@ckeditor/ckeditor5-utils/src/env';
/**
* Handles keystrokes which are unsafe for typing. This handler's logic is explained
* in https://github.com/ckeditor/ckeditor5-typing/issues/83#issuecomment-398690251.
*
* @param {module:core/editor/editor~Editor} editor The editor instance.
*/
export default function injectUnsafeKeystrokesHandling( editor ) {
let latestCompositionSelection = null;
const model = editor.model;
const view = editor.editing.view;
const inputCommand = editor.commands.get( 'input' );
// For Android, we want to handle keystrokes on `beforeinput` to be sure that code in `DeleteObserver` already had a chance to be fired.
if ( env.isAndroid ) {
view.document.on( 'beforeinput', ( evt, evtData ) => handleUnsafeKeystroke( evtData ), { priority: 'lowest' } );
} else {
view.document.on( 'keydown', ( evt, evtData ) => handleUnsafeKeystroke( evtData ), { priority: 'lowest' } );
}
view.document.on( 'compositionstart', handleCompositionStart, { priority: 'lowest' } );
view.document.on( 'compositionend', () => {
latestCompositionSelection = model.createSelection( model.document.selection );
}, { priority: 'lowest' } );
// Handles the keydown event. We need to guess whether such keystroke is going to result
// in typing. If so, then before character insertion happens, any selected content needs
// to be deleted. Otherwise the default browser deletion mechanism would be
// triggered, resulting in:
//
// * Hundreds of mutations which could not be handled.
// * But most importantly, loss of control over how the content is being deleted.
//
// The method is used in a low-priority listener, hence allowing other listeners (e.g. delete or enter features)
// to handle the event.
//
// @param {module:engine/view/observer/keyobserver~KeyEventData} evtData
function handleUnsafeKeystroke( evtData ) {
const doc = model.document;
const isComposing = view.document.isComposing;
const isSelectionUnchanged = latestCompositionSelection && latestCompositionSelection.isEqual( doc.selection );
// Reset stored composition selection.
latestCompositionSelection = null;
// By relying on the state of the input command we allow disabling the entire input easily
// by just disabling the input command. We could’ve used here the delete command but that
// would mean requiring the delete feature which would block loading one without the other.
// We could also check the editor.isReadOnly property, but that wouldn't allow to block
// the input without blocking other features.
if ( !inputCommand.isEnabled ) {
return;
}
if ( isNonTypingKeystroke( evtData ) || doc.selection.isCollapsed ) {
return;
}
// If during composition, deletion should be prevented as it may remove composed sequence (#83).
if ( isComposing && evtData.keyCode === 229 ) {
return;
}
// If there is a `keydown` event fired with '229' keycode it might be related
// to recent composition. Check if selection is the same as upon ending recent composition,
// if so do not remove selected content as it will remove composed sequence (#83).
if ( !isComposing && evtData.keyCode === 229 && isSelectionUnchanged ) {
return;
}
deleteSelectionContent();
}
// Handles the `compositionstart` event. It is used only in special cases to remove the contents
// of a non-collapsed selection so composition itself does not result in complex mutations.
//
// The special case mentioned above is a situation in which the `keydown` event is fired after
// `compositionstart` event. In such cases {@link #handleKeydown} cannot clear current selection
// contents (because it is too late and will break the composition) so the composition handler takes care of it.
function handleCompositionStart() {
const doc = model.document;
const isFlatSelection = doc.selection.rangeCount === 1 ? doc.selection.getFirstRange().isFlat : true;
// If on `compositionstart` there is a non-collapsed selection which start and end have different parents
// it means the `handleKeydown()` method did not remove its contents. It happens usually because
// of different order of events (`compositionstart` before `keydown` - in Safari). In such cases
// we need to remove selection contents on composition start (#83).
if ( doc.selection.isCollapsed || isFlatSelection ) {
return;
}
deleteSelectionContent();
}
function deleteSelectionContent() {
const buffer = inputCommand.buffer;
buffer.lock();
const batch = buffer.batch;
inputCommand._batches.add( batch );
model.enqueueChange( batch, () => {
model.deleteContent( model.document.selection );
} );
buffer.unlock();
}
}
const safeKeycodes = [
getCode( 'arrowUp' ),
getCode( 'arrowRight' ),
getCode( 'arrowDown' ),
getCode( 'arrowLeft' ),
9, // Tab
16, // Shift
17, // Ctrl
18, // Alt
19, // Pause
20, // CapsLock
27, // Escape
33, // PageUp
34, // PageDown
35, // Home
36, // End,
45, // Insert,
91, // Windows,
93, // Menu key,
144, // NumLock
145, // ScrollLock,
173, // Mute/Unmute
174, // Volume up
175, // Volume down,
176, // Next song,
177, // Previous song,
178, // Stop,
179, // Play/Pause,
255 // Display brightness (increase and decrease)
];
// Function keys.
for ( let code = 112; code <= 135; code++ ) {
safeKeycodes.push( code );
}
/**
* Returns `true` if a keystroke will **not** result in "typing".
*
* For instance, keystrokes that result in typing are letters "a-zA-Z", numbers "0-9", delete, backspace, etc.
*
* Keystrokes that do not cause typing are, for instance, Fn keys (F5, F8, etc.), arrow keys (←, →, ↑, ↓),
* Tab (↹), "Windows logo key" (⊞ Win), etc.
*
* Note: This implementation is very simple and will need to be refined with time.
*
* @param {module:engine/view/observer/keyobserver~KeyEventData} keyData
* @returns {Boolean}
*/
export function isNonTypingKeystroke( keyData ) {
// Keystrokes which contain Ctrl don't represent typing.
if ( keyData.ctrlKey ) {
return true;
}
return safeKeycodes.includes( keyData.keyCode );
}