Sfoglia il codice sorgente

Handle container removal via mutations on Android.

Krzysztof Krztoń 7 anni fa
parent
commit
546bad6af5

+ 148 - 0
packages/ckeditor5-typing/src/utils/injectandroidbackspacemutationshandling.js

@@ -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' ) );
+}