Selaa lähdekoodia

Added: model.Document#getNearestSelectionPosition.

Szymon Cofalik 9 vuotta sitten
vanhempi
commit
393bec9346

+ 3 - 11
packages/ckeditor5-engine/src/controller/insertcontent.js

@@ -128,18 +128,10 @@ class Insertion {
 		if ( this.nodeToSelect ) {
 			return [ Range.createOn( this.nodeToSelect ) ];
 		} else {
-			const searchRange = new Range( Position.createAt( this.position.root ), this.position );
+			const document = this.dataController.model;
+			const selectionPosition = document.getNearestSelectionPosition( this.position );
 
-			for ( const position of searchRange.getPositions( { direction: 'backward' } ) ) {
-				if ( this.schema.check( { name: '$text', inside: position } ) ) {
-					return [ new Range( position ) ];
-				}
-			}
-
-			// As a last resort, simply return the current position.
-			// See the "should not break when autoparagraphing of text is not possible" test for
-			// a case which triggers this condition.
-			return [ new Range( this.position ) ];
+			return [ new Range( selectionPosition ) ];
 		}
 	}
 

+ 51 - 7
packages/ckeditor5-engine/src/model/document.js

@@ -14,6 +14,7 @@ import Batch from './batch.js';
 import History from './history.js';
 import LiveSelection from './liveselection.js';
 import Schema from './schema.js';
+import TreeWalker from './treewalker.js';
 import clone from '../../utils/lib/lodash/clone.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
@@ -275,6 +276,54 @@ export default class Document {
 	}
 
 	/**
+	 * Basing on given `position`, finds and returns a {@link engine.model.Position Position} instance that is nearest
+	 * to that `position` and is a correct position for selection. A position is correct for selection if
+	 * text node can be placed at that position.
+	 *
+	 * If no correct position is found, the first position in given `position` root is returned. This can happen if no node
+	 * has been added to the root or it may mean incorrect model document state.
+	 *
+	 * @param {engine.model.Position} position Reference position where selection position should be looked for.
+	 * @returns {engine.model.Position|null} Nearest selection position.
+	 */
+	getNearestSelectionPosition( position ) {
+		if ( this.schema.check( { name: '$text', inside: position } ) ) {
+			return Position.createFromPosition( position );
+		}
+
+		const backwardWalker = new TreeWalker( { startPosition: position, direction: 'backward' } );
+		const forwardWalker = new TreeWalker( { startPosition: position } );
+
+		let done = false;
+
+		while ( !done ) {
+			done = true;
+
+			let step = backwardWalker.next();
+
+			if ( !step.done ) {
+				if ( this.schema.check( { name: '$text', inside: step.value.nextPosition } ) ) {
+					return step.value.nextPosition;
+				}
+
+				done = false;
+			}
+
+			step = forwardWalker.next();
+
+			if ( !step.done ) {
+				if ( this.schema.check( { name: '$text', inside: step.value.nextPosition } ) ) {
+					return step.value.nextPosition;
+				}
+
+				done = false;
+			}
+		}
+
+		return new Position( position.root, [ 0 ] );
+	}
+
+	/**
 	 * Custom toJSON method to solve child-parent circular dependencies.
 	 *
 	 * @returns {Object} Clone of this object with the document property changed to string.
@@ -317,15 +366,10 @@ export default class Document {
 		const defaultRoot = this._getDefaultRoot();
 
 		// Find the first position where the selection can be put.
-		for ( let position of Range.createIn( defaultRoot ).getPositions() ) {
-			if ( this.schema.check( { name: '$text', inside: position } ) ) {
-				return new Range( position, position );
-			}
-		}
-
 		const position = new Position( defaultRoot, [ 0 ] );
+		const selectionPosition = this.getNearestSelectionPosition( position );
 
-		return new Range( position, position );
+		return new Range( selectionPosition );
 	}
 
 	/**

+ 3 - 16
packages/ckeditor5-engine/src/model/liveselection.js

@@ -8,7 +8,6 @@ import Range from './range.js';
 import LiveRange from './liverange.js';
 import Text from './text.js';
 import TextProxy from './textproxy.js';
-import TreeWalker from './treewalker.js';
 import toMap from '../../utils/tomap.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import log from '../../utils/log.js';
@@ -622,23 +621,11 @@ export default class LiveSelection extends Selection {
 		newPath[ newPath.length - 1 ] -= gyPath[ 1 ];
 
 		const newPosition = new Position( oldRange.root, newPath );
-
-		const walker = new TreeWalker( { startPosition: newPosition, direction: 'backward' } );
-		let newRange = null;
-
-		for ( let value of walker ) {
-			if ( this._document.schema.check( { name: '$text', inside: value.previousPosition } ) ) {
-				newRange = new Range( value.previousPosition, value.previousPosition );
-				break;
-			}
-		}
-
-		if ( !newRange ) {
-			newRange = this._document._getDefaultRange();
-		}
+		const selectionPosition = this._document.getNearestSelectionPosition( newPosition );
+		const newRange = this._prepareRange( new Range( selectionPosition ) );
 
 		const index = this._ranges.indexOf( gyRange );
-		this._ranges.splice( index, 1, this._prepareRange( newRange ) );
+		this._ranges.splice( index, 1, newRange );
 
 		gyRange.detach();
 	}

+ 0 - 25
packages/ckeditor5-engine/tests/controller/insertcontent.js

@@ -501,31 +501,6 @@ describe( 'DataController', () => {
 				expect( getData( doc ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
 			} );
 		} );
-
-		describe( 'special schema configurations', () => {
-			beforeEach( () => {
-				doc = new Document();
-				doc.createRoot();
-
-				dataController = new DataController( doc );
-			} );
-
-			it( 'should not break when autoparagraphing of text is not possible', () => {
-				const schema = doc.schema;
-
-				schema.registerItem( 'noTextAllowed' );
-				schema.registerItem( 'object' );
-
-				schema.allow( { name: 'noTextAllowed', inside: '$root' } );
-				schema.allow( { name: 'object', inside: 'noTextAllowed' } );
-
-				schema.objects.add( 'object' );
-
-				setData( doc, '<noTextAllowed>[<object></object>]</noTextAllowed>' );
-				insertHelper( 'foo' );
-				expect( getData( doc ) ).to.equal( '<noTextAllowed>[]</noTextAllowed>' );
-			} );
-		} );
 	} );
 
 	// @param {engine.model.Item|String} content

+ 47 - 0
packages/ckeditor5-engine/tests/model/document/document.js

@@ -8,8 +8,10 @@
 import Document from 'ckeditor5/engine/model/document.js';
 import Schema from 'ckeditor5/engine/model/schema.js';
 import RootElement from 'ckeditor5/engine/model/rootelement.js';
+import Element from 'ckeditor5/engine/model/element.js';
 import Batch from 'ckeditor5/engine/model/batch.js';
 import Delta from 'ckeditor5/engine/model/delta/delta.js';
+import Position from 'ckeditor5/engine/model/position.js';
 import Range from 'ckeditor5/engine/model/range.js';
 import CKEditorError from 'ckeditor5/utils/ckeditorerror.js';
 import count from 'ckeditor5/utils/count.js';
@@ -274,6 +276,51 @@ describe( 'Document', () => {
 		} );
 	} );
 
+	describe( 'getNearestSelectionPosition', () => {
+		let root;
+
+		beforeEach( () => {
+			doc.schema.registerItem( 'paragraph', '$block' );
+			root = doc.createRoot();
+		} );
+
+		it( 'should return equal position if text node can be placed at that position', () => {
+			root.appendChildren( new Element( 'paragraph' ) );
+
+			const position = new Position( root, [ 0, 0 ] );
+			const selectionPosition = doc.getNearestSelectionPosition( position );
+
+			expect( position.isEqual( selectionPosition ) ).to.be.true;
+		} );
+
+		it( 'should return a position before if it is a closer position at which text node can be placed', () => {
+			root.appendChildren( [ new Element( 'paragraph' ), new Element( 'paragraph' ) ] );
+
+			const position = new Position( root, [ 1 ] );
+			const selectionPosition = doc.getNearestSelectionPosition( position );
+
+			expect( selectionPosition.path ).to.deep.equal( [ 0, 0 ] );
+		} );
+
+		it( 'should return a position after if it is a closer position at which text node can be placed', () => {
+			root.appendChildren( [ new Element( 'paragraph' ), new Element( 'image' ), new Element( 'paragraph' ) ] );
+
+			const position = new Position( root, [ 2 ] );
+			const selectionPosition = doc.getNearestSelectionPosition( position );
+
+			expect( selectionPosition.path ).to.deep.equal( [ 2, 0 ] );
+		} );
+
+		it( 'should return first position in root if there is no position where text node can be placed', () => {
+			root.appendChildren( new Element( 'table' ) );
+
+			const position = new Position( root, [ 0, 0 ] );
+			const selectionPosition = doc.getNearestSelectionPosition( position );
+
+			expect( selectionPosition.path ).to.deep.equal( [ 0 ] );
+		} );
+	} );
+
 	describe( '_getDefaultRoot', () => {
 		it( 'should return graveyard root if there are no other roots in the document', () => {
 			expect( doc._getDefaultRoot() ).to.equal( doc.graveyard );