|
|
@@ -0,0 +1,148 @@
|
|
|
+/**
|
|
|
+ * @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;
|
|
|
+
|
|
|
+ let previousSelection = null;
|
|
|
+ let currentSelection = new Selection( model.document.selection );
|
|
|
+
|
|
|
+ view.document.on( 'selectionChange', 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.
|
|
|
+ function handleSelectionChange() {
|
|
|
+ previousSelection = currentSelection;
|
|
|
+ currentSelection = new Selection( model.document.selection );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handles specific DOM 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() {
|
|
|
+ const selection = shouldUsePreviousSelection( currentSelection, previousSelection ) ? previousSelection : currentSelection;
|
|
|
+
|
|
|
+ if ( shouldUsePreviousSelection( currentSelection, previousSelection ) ) {
|
|
|
+ model.enqueueChange( writer => {
|
|
|
+ writer.setSelection( selection );
|
|
|
+ } );
|
|
|
+ }
|
|
|
+
|
|
|
+ editor.execute( 'delete' );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Whether previously saved selection should be used to remove content instead of the current one.
|
|
|
+// Previous selection will be used if it exists is non-collapsed and has the same last position as the current selection.
|
|
|
+//
|
|
|
+// 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.
|
|
|
+//
|
|
|
+// @private
|
|
|
+// @param {module:engine/model/selection~Selection} currentSelection
|
|
|
+// @param {module:engine/model/selection~Selection} previousSelection
|
|
|
+// @returns {Boolean}
|
|
|
+function shouldUsePreviousSelection( currentSelection, previousSelection ) {
|
|
|
+ return previousSelection && !previousSelection.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 show just changes in a text (1st - text in `p` element was removed, 3rd - text in `h2` has 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;
|
|
|
+}
|
|
|
+
|
|
|
+// Returns true if 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' ) );
|
|
|
+}
|