浏览代码

Merge pull request #53 from ckeditor/t/ckeditor5/1243

Feature: Introduce findOptimalInsertionPostion() utility method.
Piotr Jasiun 7 年之前
父节点
当前提交
a143a735b9
共有 2 个文件被更改,包括 132 次插入2 次删除
  1. 48 2
      packages/ckeditor5-widget/src/utils.js
  2. 84 0
      packages/ckeditor5-widget/tests/utils.js

+ 48 - 2
packages/ckeditor5-widget/src/utils.js

@@ -8,7 +8,8 @@
  */
 
 import HighlightStack from './highlightstack';
-import Position from '@ckeditor/ckeditor5-engine/src/view/position';
+import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
+import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
 import IconView from '@ckeditor/ckeditor5-ui/src/icon/iconview';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 
@@ -240,6 +241,51 @@ export function toWidgetEditable( editable, writer ) {
 	return editable;
 }
 
+/**
+ * Returns a model position which is optimal (in terms of UX) for inserting a widget block.
+ *
+ * For instance, if a selection is in the middle of a paragraph, the position before this paragraph
+ * will be returned so that it is not split. If the selection is at the end of a paragraph,
+ * the position after this paragraph will be returned.
+ *
+ * Note: If the selection is placed in an empty block, that block will be returned. If that position
+ * is then passed to {@link module:engine/model/model~Model#insertContent},
+ * the block will be fully replaced by the image.
+ *
+ * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+ * The selection based on which the insertion position should be calculated.
+ * @returns {module:engine/model/position~Position} The optimal position.
+ */
+export function findOptimalInsertionPosition( selection ) {
+	const selectedElement = selection.getSelectedElement();
+
+	if ( selectedElement ) {
+		return ModelPosition.createAfter( selectedElement );
+	}
+
+	const firstBlock = selection.getSelectedBlocks().next().value;
+
+	if ( firstBlock ) {
+		// If inserting into an empty block – return position in that block. It will get
+		// replaced with the image by insertContent(). #42.
+		if ( firstBlock.isEmpty ) {
+			return ModelPosition.createAt( firstBlock );
+		}
+
+		const positionAfter = ModelPosition.createAfter( firstBlock );
+
+		// If selection is at the end of the block - return position after the block.
+		if ( selection.focus.isTouching( positionAfter ) ) {
+			return positionAfter;
+		}
+
+		// Otherwise return position before the block.
+		return ModelPosition.createBefore( firstBlock );
+	}
+
+	return selection.focus;
+}
+
 // Default filler offset function applied to all widget elements.
 //
 // @returns {null}
@@ -268,6 +314,6 @@ function addSelectionHandler( editable, writer ) {
 	} );
 
 	// Append the selection handler into the widget wrapper.
-	writer.insert( Position.createAt( editable ), selectionHandler );
+	writer.insert( ViewPosition.createAt( editable ), selectionHandler );
 	writer.addClass( [ 'ck-widget_selectable' ], editable );
 }

+ 84 - 0
packages/ckeditor5-widget/tests/utils.js

@@ -16,11 +16,14 @@ import {
 	getLabel,
 	toWidgetEditable,
 	setHighlightHandling,
+	findOptimalInsertionPosition,
 	WIDGET_CLASS_NAME
 } from '../src/utils';
 import UIElement from '@ckeditor/ckeditor5-engine/src/view/uielement';
 import env from '@ckeditor/ckeditor5-utils/src/env';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import Model from '@ckeditor/ckeditor5-engine/src/model/model';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
 describe( 'widget utils', () => {
 	let element, writer, viewDocument;
@@ -337,4 +340,85 @@ describe( 'widget utils', () => {
 			expect( addSpy.secondCall.args[ 1 ] ).to.equal( secondDescriptor );
 		} );
 	} );
+
+	describe( 'findOptimalInsertionPosition()', () => {
+		let model, doc;
+
+		beforeEach( () => {
+			model = new Model();
+			doc = model.document;
+
+			doc.createRoot();
+
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			model.schema.register( 'image' );
+			model.schema.register( 'span' );
+
+			model.schema.extend( 'image', {
+				allowIn: '$root',
+				isObject: true
+			} );
+
+			model.schema.extend( 'span', { allowIn: 'paragraph' } );
+			model.schema.extend( '$text', { allowIn: 'span' } );
+		} );
+
+		it( 'returns position after selected element', () => {
+			setData( model, '<paragraph>x</paragraph>[<image></image>]<paragraph>y</paragraph>' );
+
+			const pos = findOptimalInsertionPosition( doc.selection );
+
+			expect( pos.path ).to.deep.equal( [ 2 ] );
+		} );
+
+		it( 'returns position inside empty block', () => {
+			setData( model, '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );
+
+			const pos = findOptimalInsertionPosition( doc.selection );
+
+			expect( pos.path ).to.deep.equal( [ 1, 0 ] );
+		} );
+
+		it( 'returns position before block if at the beginning of that block', () => {
+			setData( model, '<paragraph>x</paragraph><paragraph>[]foo</paragraph><paragraph>y</paragraph>' );
+
+			const pos = findOptimalInsertionPosition( doc.selection );
+
+			expect( pos.path ).to.deep.equal( [ 1 ] );
+		} );
+
+		it( 'returns position before block if in the middle of that block', () => {
+			setData( model, '<paragraph>x</paragraph><paragraph>f[]oo</paragraph><paragraph>y</paragraph>' );
+
+			const pos = findOptimalInsertionPosition( doc.selection );
+
+			expect( pos.path ).to.deep.equal( [ 1 ] );
+		} );
+
+		it( 'returns position after block if at the end of that block', () => {
+			setData( model, '<paragraph>x</paragraph><paragraph>foo[]</paragraph><paragraph>y</paragraph>' );
+
+			const pos = findOptimalInsertionPosition( doc.selection );
+
+			expect( pos.path ).to.deep.equal( [ 2 ] );
+		} );
+
+		// Checking if isTouching() was used.
+		it( 'returns position after block if at the end of that block (deeply nested)', () => {
+			setData( model, '<paragraph>x</paragraph><paragraph>foo<span>bar[]</span></paragraph><paragraph>y</paragraph>' );
+
+			const pos = findOptimalInsertionPosition( doc.selection );
+
+			expect( pos.path ).to.deep.equal( [ 2 ] );
+		} );
+
+		it( 'returns selection focus if not in a block', () => {
+			model.schema.extend( '$text', { allowIn: '$root' } );
+			setData( model, 'foo[]bar' );
+
+			const pos = findOptimalInsertionPosition( doc.selection );
+
+			expect( pos.path ).to.deep.equal( [ 3 ] );
+		} );
+	} );
 } );