| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module typing/utils/injectAndroidBackspaceMutationsHandling
- */
- import Selection from '@ckeditor/ckeditor5-engine/src/model/selection';
- import diff from '@ckeditor/ckeditor5-utils/src/diff';
- import { containerChildrenMutated } from '../utils';
- /**
- * Handles mutations responsible for block elements removal generated by `Backspace` key on Android.
- *
- * @param {module:core/editor/editor~Editor} editor The editor instance.
- */
- export default function injectAndroidBackspaceMutationsHandling( editor ) {
- const model = editor.model;
- const view = editor.editing.view;
- const selectionChangeToleranceMs = 200;
- let previousSelection = null;
- let currentSelection = new Selection( model.document.selection );
- let latestSelectionChangeMs = Date.now();
- model.document.selection.on( 'change', handleSelectionChange );
- view.document.on( 'mutations', handleMutations, { priority: 'highest' } );
- // Saves current and previous selection when it changes. Saved selections are used
- // to remove correct piece of content when `Backspace` mutations are detected.
- //
- // @param {Object} evt
- function handleSelectionChange( evt ) {
- const newSelection = new Selection( evt.source );
- if ( !currentSelection.isEqual( newSelection ) ) {
- previousSelection = currentSelection;
- currentSelection = newSelection;
- latestSelectionChangeMs = Date.now();
- }
- }
- // Handles DOM mutations and checks if they should be processed as block elements removal mutations.
- //
- // @param {Object} evt
- // @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
- // module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
- function handleMutations( evt, mutations ) {
- if ( containerChildrenMutated( mutations ) && containsContainersRemoval( mutations ) ) {
- handleContainerRemovalMutations();
- // Stop mutations event propagation so other mutation handlers are not triggered.
- evt.stop();
- }
- }
- // Handles situations when mutations were generated by container removal.
- // It happens on Android devices where every typing input has `229` key code
- // and delete observer will not be triggered. In such cases we need to handle
- // container removal mutations manually.
- function handleContainerRemovalMutations() {
- if ( shouldUsePreviousSelection() ) {
- // If previous selection is used, update model selection in order
- // to use `delete` command and to make `undo` work correctly.
- model.enqueueChange( writer => {
- writer.setSelection( previousSelection );
- } );
- }
- editor.execute( 'delete' );
- }
- // Whether previously saved selection should be used instead of the current one to remove content.
- //
- // On Android devices when pressing backspace on non-collapsed selection, selection like:
- //
- // `<h1>[Foo</h1><p>Bar]</p>`
- //
- // is changed to:
- //
- // `<h1>Foo</h1><p>Bar[]</p>`
- //
- // even before `keypress` event, so in such cases we have to rely on previous selection to correctly process selected content.
- //
- // Previous selection will be used if:
- //
- // * current selection is collapsed (see example above),
- // * previous selection exists, is non-collapsed and has same ending (last position) as the current one,
- // * change of the selection happened not earlier than X milliseconds ago (see `selectionChangeToleranceMs`).
- //
- // The last check is needed, because user can manually collapse the selection on its current end and then press `Backspace`.
- // In such situations timing determines if the selection change was caused by the user or browser native behaviour.
- // However, this happens only if selection was collapsed by the user on the beginning of the paragraph (so mutations
- // still will show container removal).
- //
- // @returns {Boolean}
- function shouldUsePreviousSelection() {
- return Date.now() - latestSelectionChangeMs < selectionChangeToleranceMs &&
- previousSelection && !previousSelection.isCollapsed && currentSelection.isCollapsed &&
- currentSelection.getLastPosition().isEqual( previousSelection.getLastPosition() );
- }
- }
- // Checks whether mutations array contains mutation generated by container/containers removal.
- // For example mutations generated on Android when pressing `backspace` on the beginning of the line:
- //
- // <h1>Header1</h1>
- // <p>{}Paragraph</p>
- //
- // are:
- //
- // [
- // { newChildren: [], oldChildren: [ 'Paragraph' ], node: P, type: 'children' },
- // { newChildren: [ ContainerElement ], oldChildren: [ ContainerElement, ContainerElement ], node: Root, type: 'children' },
- // { newChildren: [ 'Heading 1Paragraph' ], oldChildren: [ 'Heading 1' ], node: H1, type: 'children' }
- // ]
- //
- // The 1st and 3rd mutations are just changes in a text (1st - text in `p` element was removed, 3rd - text in `h2` was changed)
- // and the 2nd one shows that one `ContainerElement` was removed. We have to recognize if mutations like 2nd one are present.
- // Based on that heuristic mutations are treated as the one removing container element.
- //
- // @private
- // @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
- // module:engine/view/observer/mutationobserver~MutatedChildren>} mutations
- // @returns {Boolean}
- function containsContainersRemoval( mutations ) {
- for ( const mutation of mutations ) {
- if ( mutation.type !== 'children' ) {
- continue;
- }
- const childrenBefore = mutation.oldChildren;
- const childrenAfter = mutation.newChildren;
- // Check if only containers were present before the mutation.
- if ( !hasOnlyContainers( childrenBefore ) ) {
- continue;
- }
- const diffResult = diff( childrenBefore, childrenAfter );
- // Check if there was only removing in that mutation without any insertions.
- const hasDelete = diffResult.some( item => item === 'delete' );
- const hasInsert = diffResult.some( item => item === 'insert' );
- if ( hasDelete && !hasInsert ) {
- return true;
- }
- }
- return false;
- }
- // Whether provided array contains only nodes of `containerElement` type.
- //
- // @private
- // @param {Array.<module:engine/model/node~Node>} children
- // @returns {Boolean}
- function hasOnlyContainers( children ) {
- return children.every( child => child.is( 'containerElement' ) );
- }
|