浏览代码

Introduce mapViewPositionInsideInlineElement() default view-to-model position mapper.

Maciej Gołaszewski 6 年之前
父节点
当前提交
ae6d003710

+ 4 - 0
packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -16,6 +16,7 @@ import { clearAttributes, convertCollapsedSelection, convertRangeSelection, inse
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import { convertSelectionChange } from '../conversion/upcasthelpers';
+import { mapViewPositionInsideInlineElement } from '../conversion/mapper-helpers';
 
 /**
  * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
@@ -122,6 +123,9 @@ export default class EditingController {
 
 			return viewRoot;
 		} );
+
+		// Attach view-to-model position mapper for inline elements (inline widget).
+		this.mapper.on( 'viewToModelPosition', mapViewPositionInsideInlineElement( this.model ) );
 	}
 
 	/**

+ 52 - 0
packages/ckeditor5-engine/src/conversion/mapper-helpers.js

@@ -0,0 +1,52 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/conversion/mapper-helpers
+ */
+
+/**
+ * Maps view position to model element if view positions is inside inLine element - ie. inline widget.
+ *
+ *		editor.editing.mapper.on( 'viewToModel', mapViewPositionInsideInlineElement( model ) );
+ *
+ *
+ * It will try to map view offset of selection to an expected model position in order to create consistent selection.
+ *
+ * If DOM selection starts before inline element then view position offset will be 0 and be considered at it start and thus model position
+ * will be placed "after" element to select it:
+ *
+ * 1. When selection starts before inline widget (offset=0):
+ *
+ *		+- anchor                +- focus (mapped)
+ *		|                        |
+ *		<p>f[oo <span class="widget">]widget</span></p>        map to:        <paragraph>f[oo <inline-widget></inline-widget>]</paragraph>
+ *
+ * 2. When selection start before inline widget (offset=0):
+ *
+ *		+- focus (mapped)        +- anchor
+ *		|                        |
+ *		<p><span class="widget">widget[</span> ba]r</p>        map to:        <paragraph>[<inline-widget></inline-widget> ba]r</paragraph>
+ *
+ * @param {module:engine/model/model~Model} model
+ * @return {Function}
+ */
+export function mapViewPositionInsideInlineElement( model ) {
+	const schema = model.schema;
+
+	return ( evt, data ) => {
+		const { mapper, viewPosition } = data;
+
+		const viewParent = viewPosition.parent;
+
+		const modelParent = mapper.toModelElement( viewParent );
+
+		if ( modelParent && schema.isInline( modelParent ) ) {
+			const isAtStart = viewPosition.offset === 0;
+
+			data.modelPosition = model.createPositionAt( modelParent, isAtStart ? 'after' : 'before' );
+		}
+	};
+}

+ 96 - 0
packages/ckeditor5-engine/tests/conversion/mapper-helpers.js

@@ -0,0 +1,96 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Model from '../../src/model/model';
+
+import { setData as modelSetData } from '../../src/dev-utils/model';
+import View from '../../src/view/view';
+import createViewRoot from '../view/_utils/createroot';
+import { setData as viewSetData } from '../../src/dev-utils/view';
+import Mapper from '../../src/conversion/mapper';
+import ViewPosition from '../../src/view/position';
+import { mapViewPositionInsideInlineElement } from '../../src/conversion/mapper-helpers';
+
+describe( 'mapper-helpers', () => {
+	describe( 'mapViewPositionInsideInlineElement()', () => {
+		let model, view, viewDocument, mapper, modelRoot, viewRoot, viewInlineWidget, viewP, modelParagraph;
+
+		beforeEach( () => {
+			model = new Model();
+			modelRoot = model.document.createRoot();
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			model.schema.register( 'inline-widget', { isInline: true, isObject: true, allowWhere: '$text' } );
+
+			modelSetData( model, '<paragraph>foo<inline-widget></inline-widget>bar</paragraph>' );
+
+			view = new View();
+			viewDocument = view.document;
+			viewRoot = createViewRoot( viewDocument, 'div', 'main' );
+
+			viewSetData( view, '<p>foo<placeholder>widget description</placeholder>bar</p>' );
+
+			viewP = viewRoot.getChild( 0 );
+			viewInlineWidget = viewP.getChild( 1 );
+			modelParagraph = modelRoot.getChild( 0 );
+
+			mapper = new Mapper();
+			mapper.bindElements( modelRoot, viewRoot );
+			mapper.bindElements( modelParagraph, viewRoot.getChild( 0 ) );
+			mapper.bindElements( modelParagraph.getChild( 1 ), viewInlineWidget );
+
+			mapper.on( 'viewToModelPosition', mapViewPositionInsideInlineElement( model ) );
+		} );
+
+		it( 'should return position after inline-widget when view position is at start of inline-widget', () => {
+			const viewPosition = new ViewPosition( viewInlineWidget, 0 );
+
+			const modelPosition = mapper.toModelPosition( viewPosition );
+
+			expect( modelPosition.parent ).to.equal( modelParagraph );
+			// The offset in <paragraph> should be after the inline-widget:
+			// PARAGRAPH
+			//   |- foo            before: [ 0 ]         after: [ 3 ]
+			//   |- INLINE-WIDGET  before: [ 3 ]         after: [ 4 ]
+			//   |- bar            before: [ 4 ]         after: [ 7 ]
+			expect( modelPosition.offset ).to.equal( 4 );
+		} );
+
+		it( 'should return position before inline-widget when view position is not at start of inline-widget', () => {
+			const viewPosition = new ViewPosition( viewInlineWidget, 2 ); // in real-case scenarios is 1 even if placeholder has text.
+
+			const modelPosition = mapper.toModelPosition( viewPosition );
+
+			expect( modelPosition.parent ).to.equal( modelParagraph );
+			// The offset in <paragraph> should be before the inline-widget:
+			// PARAGRAPH
+			//   |- foo            before: [ 0 ]         after: [ 3 ]
+			//   |- INLINE-WIDGET  before: [ 3 ]         after: [ 4 ]
+			//   |- bar            before: [ 4 ]         after: [ 7 ]
+			expect( modelPosition.offset ).to.equal( 3 );
+		} );
+
+		it( 'should do nothing if position maps to text node', () => {
+			const viewPosition = new ViewPosition( viewP.getChild( 0 ), 1 );
+
+			const data = { viewPosition, mapper };
+
+			const mapperHelper = mapViewPositionInsideInlineElement( model );
+			mapperHelper( {}, data );
+
+			expect( data.modelPosition ).to.be.undefined;
+		} );
+
+		it( 'should do nothing if position maps to a non-inline element', () => {
+			const viewPosition = new ViewPosition( viewP, 0 );
+
+			const data = { viewPosition, mapper };
+
+			const mapperHelper = mapViewPositionInsideInlineElement( model );
+			mapperHelper( {}, data );
+
+			expect( data.modelPosition ).to.be.undefined;
+		} );
+	} );
+} );