Selaa lähdekoodia

Made the tests pass.

Piotrek Koszuliński 9 vuotta sitten
vanhempi
sitoutus
54521aac39

+ 1 - 35
packages/ckeditor5-engine/src/datacontroller.js

@@ -19,7 +19,7 @@ import ViewDocumentFragment from './view/documentfragment.js';
 import ModelRange from './model/range.js';
 import ModelPosition from './model/position.js';
 
-import { stringify as stringifyModel } from '../engine/dev-utils/model.js';
+import insertContent from './datacontroller/insertcontent.js';
 
 /**
  * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
@@ -227,37 +227,3 @@ export default class DataController {
 }
 
 mix( DataController, EmitterMixin );
-
-/**
- * TODO
- *
- * @method engine.dataController.insertContent
- * @param {engine.DataController} dataController
- * @param {engine.model.Batch} batch Batch to which deltas will be added.
- * @param {engine.model.Selection} selection Selection into which the content should be inserted.
- * The selection should be collapsed.
- * @param {engine.model.DocumentFragment} content The content to insert.
- */
-export function insertContent( dataController, batch, selection, content ) {
-	if ( !selection.isCollapsed ) {
-		dataController.model.composer.deleteContents( batch, selection, {
-			merge: true
-		} );
-	}
-
-	// Convert the pasted content to a model document fragment.
-	// Convertion is contextual, but in this case we need an "all allowed" context and for that
-	// we use the $clipboardHolder item.
-	const modelFragment = dataController.viewToModel.convert( content, {
-		context: [ '$clipboardHolder' ]
-	} );
-
-	console.log( 'insert (model):' ); // jshint ignore:line
-	console.log( stringifyModel( modelFragment ) ); // jshint ignore:line
-
-	for ( const node of modelFragment ) {
-		const clonedNode = node.clone();
-
-		batch.insert( selection.anchor, clonedNode );
-	}
-}

+ 284 - 0
packages/ckeditor5-engine/src/datacontroller/insertcontent.js

@@ -0,0 +1,284 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import { stringify as stringifyModel } from '../dev-utils/model.js';
+
+import Position from '../model/position.js';
+import LivePosition from '../model/liveposition.js';
+import Text from '../model/text.js';
+import Element from '../model/element.js';
+// import RootElement from '../model/rootelement.js';
+import Range from '../model/range.js';
+import log from '../../utils/log.js';
+
+window.stringify = stringifyModel; // jshint ignore:line
+
+/**
+ * TODO
+ *
+ * @method engine.dataController.insertContent
+ * @param {engine.DataController} dataController
+ * @param {engine.model.Batch} batch Batch to which deltas will be added.
+ * @param {engine.model.Selection} selection Selection into which the content should be inserted.
+ * The selection should be collapsed.
+ * @param {engine.model.DocumentFragment} content The content to insert.
+ */
+export default function insertContent( dataController, batch, selection, content ) {
+	if ( !selection.isCollapsed ) {
+		dataController.model.composer.deleteContents( batch, selection, {
+			merge: true
+		} );
+	}
+
+	// Convert the pasted content to a model document fragment.
+	// Convertion is contextual, but in this case we need an "all allowed" context and for that
+	// we use the $clipboardHolder item.
+	const modelFragment = dataController.viewToModel.convert( content, {
+		context: [ '$clipboardHolder' ]
+	} );
+
+	const insertion = new Insertion( dataController, batch, selection.anchor );
+
+	insertion.handleNodes( modelFragment.getChildren() );
+
+	selection.setRanges( insertion.getSelectionRanges() );
+}
+
+class Insertion {
+	constructor( dataController, batch, position ) {
+		this.dataController = dataController;
+		this.batch = batch;
+		this.position = position;
+		this.canMergeWith = new Set( [ this.position.parent ] );
+
+		this.schema = dataController.model.schema;
+	}
+
+	handleNodes( nodes, parentContext ) {
+		nodes = Array.from( nodes );
+
+		if ( !parentContext ) {
+			parentContext = {
+				isFirst: true,
+				isLast: true
+			};
+		}
+
+		for ( let i = 0; i < nodes.length; i++ ) {
+			const node = nodes[ i ].clone();
+
+			this.handleNode( node, {
+				isFirst: i === 0 && parentContext.isFirst,
+				isLast: ( i === ( nodes.length - 1 ) ) && parentContext.isLast
+			} );
+		}
+	}
+
+	handleNode( node, context = {} ) {
+		context.isElement = ( node instanceof Element );
+		context.isObject = this._checkIsObject( node );
+
+		if ( context.isObject ) {
+			this._handleObject( node );
+
+			return;
+		}
+
+		// if ( this._checkShouldReplaceParent( node, context ) ) {
+		// 	const parentNode = this.position.parent;
+
+		// 	this.position = Position.createBefore( parentNode );
+		// 	this.batch.remove( parentNode );
+		// }
+
+		const isAllowed = this._splitToAllowedPosition( node, context );
+
+		if ( !isAllowed ) {
+			if ( context.isElement ) {
+				this.handleNodes( node.getChildren(), context );
+			} else {
+				const container = new Element( 'paragraph' );
+				container.appendChildren( node );
+
+				this.handleNode( container, context );
+			}
+
+			return;
+		}
+
+		this._insert( node );
+
+		if ( context.isElement ) {
+			const mergeLeft = context.isFirst && ( node.previousSibling instanceof Element ) && this.canMergeWith.has( node.previousSibling );
+			const mergeRight = context.isLast && ( node.nextSibling instanceof Element ) && this.canMergeWith.has( node.nextSibling );
+			const mergePosLeft = LivePosition.createBefore( node );
+			const mergePosRight = LivePosition.createAfter( node );
+
+			if ( mergeLeft ) {
+				const position = LivePosition.createFromPosition( this.position );
+
+				this.batch.merge( mergePosLeft );
+
+				this.position = Position.createFromPosition( position );
+				position.detach();
+			}
+
+			if ( mergeRight ) {
+				let position;
+
+				if ( this.position.isEqual( mergePosRight ) ) {
+					this.position = Position.createAt( mergePosRight.nodeBefore, 'end' );
+
+					// <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when stick to previous)
+					// <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
+					position = new LivePosition( this.position.root, this.position.path, 'STICKS_TO_PREVIOUS' );
+				} else {
+					position = LivePosition.createFromPosition( this.position );
+				}
+
+				this.batch.merge( mergePosRight );
+
+				this.position = Position.createFromPosition( position );
+				position.detach();
+			}
+
+			mergePosLeft.detach();
+			mergePosRight.detach();
+		}
+	}
+
+	getSelectionRanges() {
+		if ( this.nodeToSelect ) {
+			return [ Range.createOn( this.nodeToSelect ) ];
+		} else {
+			const searchRange = new Range( Position.createAt( this.position.root ), this.position );
+
+			for ( const position of searchRange.getPositions( { direction: 'backward' } ) ) {
+				if ( this.schema.check( { name: '$text', inside: position } ) ) {
+					return [ new Range( position ) ];
+				}
+			}
+
+			log.error( 'insertcontent-no-selection-position: It was not possible to find a position for the resulting selection.' );
+
+			return [ new Range( this.position ) ];
+		}
+	}
+
+	_handleObject( node ) {
+		if ( this._splitToAllowedPosition( node ) ) {
+			this._insert( node );
+		}
+	}
+
+	_insert( node ) {
+		if ( !this._checkIsAllowed( node, [ this.position.parent ] ) ) {
+			// The code should never get here. If it does, it means that we have a bug which
+			// may be silent if we don't log this (the model may still work after insertion).
+			log.error(
+				'insertcontent-wrong-position: The node cannot be inserted on the given position.',
+				{ node, position: this.position }
+			);
+
+			return;
+		}
+
+		const livePos = LivePosition.createFromPosition( this.position );
+
+		this.batch.insert( this.position, node );
+
+		this.position = Position.createFromPosition( livePos );
+		livePos.detach();
+
+		// The last inserted "block" object should be selected
+		// because we can't put a collapsed selection after it.
+		if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) {
+			this.nodeToSelect = node;
+		} else {
+			this.nodeToSelect = null;
+		}
+	}
+
+	_splitToAllowedPosition( node ) {
+		const allowedIn = this._getAllowedIn( node, this.position.parent );
+
+		if ( !allowedIn ) {
+			return false;
+		}
+
+		while ( allowedIn != this.position.parent ) {
+			if ( this.position.isAtStart ) {
+				const parent = this.position.parent;
+				this.position = Position.createBefore( parent );
+
+				// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
+				// We can remove the element after moving selection out of it.
+				if ( parent.isEmpty ) {
+					this.batch.remove( parent );
+				}
+			} else if ( this.position.isAtEnd ) {
+				this.position = Position.createAfter( this.position.parent );
+			} else {
+				const tempPos = Position.createAfter( this.position.parent );
+
+				this.batch.split( this.position );
+
+				this.position = tempPos;
+
+				this.canMergeWith.add( this.position.nodeAfter );
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Get the element or the first of its ancestors in which the given node is allowed.
+	 *
+	 * @param {engine.model.Node}
+	 * @param {engine.model.Element} element
+	 * @returns {engine.model.Element|null}
+	 */
+	_getAllowedIn( node, element ) {
+		if ( this._checkIsAllowed( node, [ element ] ) ) {
+			return element;
+		}
+
+		if ( element.parent ) {
+			return this._getAllowedIn( node, element.parent );
+		}
+
+		return null;
+	}
+
+	/**
+	 * @param {engine.model.Node}
+	 * @param {engine.model.SchemaPath} path
+	 */
+	_checkIsAllowed( node, path ) {
+		return this.schema.check( { name: this._getNodeSchemaName( node ), inside: path } );
+	}
+
+	_checkIsObject( node ) {
+		return this.schema.objects.has( this._getNodeSchemaName( node ) );
+	}
+
+	// _checkShouldReplaceParent( node, context ) {
+	// 	const parentNode = this.position.parent;
+
+	// 	return context.isElement &&
+	// 		( parentNode instanceof Element ) &&
+	// 		parentNode.isEmpty &&
+	// 		!this._checkIsAllowed( node, [ parentNode ] );
+	// }
+
+	_getNodeSchemaName( node ) {
+		if ( node instanceof Text ) {
+			return '$text';
+		}
+
+		return node.name;
+	}
+}

+ 37 - 17
packages/ckeditor5-engine/tests/datacontroller/insertcontent.js

@@ -6,7 +6,8 @@
 /* bender-tags: model */
 
 import Document from '/ckeditor5/engine/model/document.js';
-import { default as DataController, insertContent } from '/ckeditor5/engine/datacontroller.js';
+import DataController from '/ckeditor5/engine/datacontroller.js';
+import insertContent from '/ckeditor5/engine/datacontroller/insertcontent.js';
 
 import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import ModelDocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
@@ -210,28 +211,28 @@ describe( 'DataController', () => {
 					'inserts one paragraph into an empty paragraph',
 					'<paragraph>xyz</paragraph>',
 					'<paragraph>[]</paragraph>',
-					'<paragraph>xyz</paragraph>'
+					'<paragraph>xyz[]</paragraph>'
 				);
 
 				test(
 					'inserts one block into a fully selected content',
 					'<heading2>xyz</heading2>',
 					'<heading1>[foo</heading1><paragraph>bar]</paragraph>',
-					'<heading2>xyz</heading2>'
+					'<heading2>xyz[]</heading2>'
 				);
 
 				test(
 					'inserts one heading',
 					'<heading1>xyz</heading1>',
 					'<paragraph>f[]oo</paragraph>',
-					'<paragraph>f</paragraph><heading1>xyz</heading1><paragraph>[]oo</paragraph>'
+					'<paragraph>fxyz[]oo</paragraph>'
 				);
 
 				test(
 					'inserts two headings',
-					'<heading1>xyz1</heading1><heading1>xyz2</heading1>',
+					'<heading1>xxx</heading1><heading1>yyy</heading1>',
 					'<paragraph>f[]oo</paragraph>',
-					'<paragraph>f</paragraph><heading1>xyz1</heading1><heading1>xyz2</heading1><paragraph>[]oo</paragraph>'
+					'<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>'
 				);
 
 				test(
@@ -261,49 +262,56 @@ describe( 'DataController', () => {
 					'inserts text + paragraph',
 					'xxx<paragraph>yyy</paragraph>',
 					'<paragraph>f[]oo</paragraph>',
-					'<paragraph>fxxx</paragraph><paragraph>yyy</paragraph><paragraph>[]oo</paragraph>'
+					'<paragraph>fxxx</paragraph><paragraph>yyy[]oo</paragraph>'
 				);
 
 				test(
 					'inserts text + paragraph (at the beginning)',
 					'xxx<paragraph>yyy</paragraph>',
 					'<paragraph>[]foo</paragraph>',
-					'<paragraph>xxx</paragraph><paragraph>yyy</paragraph><paragraph>[]foo</paragraph>'
+					'<paragraph>xxx</paragraph><paragraph>yyy[]foo</paragraph>'
 				);
 
 				test(
 					'inserts text + paragraph (at the end)',
 					'xxx<paragraph>yyy</paragraph>',
 					'<paragraph>foo[]</paragraph>',
-					'<paragraph>fooxxx</paragraph><paragraph>yyy</paragraph><paragraph>[]</paragraph>'
+					'<paragraph>fooxxx</paragraph><paragraph>yyy[]</paragraph>'
 				);
 
 				test(
 					'inserts paragraph + text',
 					'<paragraph>yyy</paragraph>xxx',
 					'<paragraph>f[]oo</paragraph>',
-					'<paragraph>f</paragraph><paragraph>xyz</paragraph><paragraph>[]oo</paragraph>'
+					'<paragraph>fyyy</paragraph><paragraph>xxx[]oo</paragraph>'
+				);
+
+				test(
+					'inserts paragraph + text + inlineWidget + text',
+					'<paragraph>yyy</paragraph>xxx<inlineWidget></inlineWidget>zzz',
+					'<paragraph>f[]oo</paragraph>',
+					'<paragraph>fyyy</paragraph><paragraph>xxx<inlineWidget></inlineWidget>zzz[]oo</paragraph>'
 				);
 
 				test(
 					'inserts paragraph + text (at the beginning)',
 					'<paragraph>yyy</paragraph>xxx',
-					'<paragraph>f[]oo</paragraph>',
-					'<paragraph>f</paragraph><paragraph>xyz</paragraph><paragraph>[]oo</paragraph>'
+					'<paragraph>[]foo</paragraph>',
+					'<paragraph>yyy</paragraph><paragraph>xxx[]foo</paragraph>'
 				);
 
 				test(
 					'inserts paragraph + text (at the end)',
 					'<paragraph>yyy</paragraph>xxx',
-					'<paragraph>f[]oo</paragraph>',
-					'<paragraph>f</paragraph><paragraph>xyz</paragraph><paragraph>[]oo</paragraph>'
+					'<paragraph>foo[]</paragraph>',
+					'<paragraph>fooyyy</paragraph><paragraph>xxx[]</paragraph>'
 				);
 
 				test(
 					'inserts text + heading',
 					'xxx<heading1>yyy</heading1>',
 					'<paragraph>f[]oo</paragraph>',
-					'<paragraph>fxxx</paragraph><heading1>yyy</heading1><paragraph>[]oo</paragraph>'
+					'<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>'
 				);
 
 				test(
@@ -361,7 +369,7 @@ describe( 'DataController', () => {
 					'inserts inline object',
 					'<inlineWidget></inlineWidget>',
 					'<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>',
-					'<paragraph>foo</paragraph><paragraph>[<inlineWidget></inlineWidget>]</paragraph><paragraph>bar</paragraph>'
+					'<paragraph>foo</paragraph><paragraph><inlineWidget></inlineWidget>[]</paragraph><paragraph>bar</paragraph>'
 				);
 			} );
 
@@ -398,7 +406,7 @@ describe( 'DataController', () => {
 					'inserts inline object',
 					'<inlineWidget></inlineWidget>',
 					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>',
-					'<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>'
+					'<paragraph>foo<inlineWidget></inlineWidget>[]bar</paragraph>'
 				);
 
 				test(
@@ -424,11 +432,16 @@ describe( 'DataController', () => {
 				// Let's use table as an example of content which needs to be filtered out.
 				schema.registerItem( 'table' );
 				schema.registerItem( 'td' );
+				schema.registerItem( 'disallowedWidget' );
 
 				schema.allow( { name: 'table', inside: '$clipboardHolder' } );
 				schema.allow( { name: 'td', inside: '$clipboardHolder' } );
 				schema.allow( { name: '$block', inside: 'td' } );
 				schema.allow( { name: '$text', inside: 'td' } );
+
+				schema.allow( { name: 'disallowedWidget', inside: '$clipboardHolder' } );
+				schema.allow( { name: '$text', inside: 'disallowedWidget' } );
+				schema.objects.add( 'disallowedWidget' );
 			} );
 
 			test(
@@ -444,6 +457,13 @@ describe( 'DataController', () => {
 				'<paragraph>f[]oo</paragraph>',
 				'<paragraph>fxxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz[]oo</paragraph>'
 			);
+
+			test(
+				'filters out disallowed objects',
+				'<disallowedWidget>xxx</disallowedWidget>',
+				'<paragraph>f[]oo</paragraph>',
+				'<paragraph>f[]oo</paragraph>'
+			);
 		} );
 	} );