浏览代码

Introduced target ranges in drop and clipboardInput events.

Piotrek Koszuliński 8 年之前
父节点
当前提交
d64d144f47

+ 38 - 3
packages/ckeditor5-clipboard/src/clipboardobserver.js

@@ -30,14 +30,49 @@ export default class ClipboardObserver extends DomEventObserver {
 		function handleInput( evt, data ) {
 			data.preventDefault();
 
-			doc.fire( 'clipboardInput', { dataTransfer: data.dataTransfer } );
+			const targetRanges = data.dropRange ? [ data.dropRange ] : Array.from( doc.selection.getRanges() );
+
+			doc.fire( 'clipboardInput', {
+				dataTransfer: data.dataTransfer,
+				targetRanges
+			} );
 		}
 	}
 
 	onDomEvent( domEvent ) {
-		this.fire( domEvent.type, domEvent, {
+		const evtData = {
 			dataTransfer: new DataTransfer( domEvent.clipboardData ? domEvent.clipboardData : domEvent.dataTransfer )
-		} );
+		};
+
+		if ( domEvent.type == 'drop' ) {
+			evtData.dropRange = getDropViewRange( this.document, domEvent );
+		}
+
+		this.fire( domEvent.type, domEvent, evtData );
+	}
+}
+
+function getDropViewRange( doc, domEvent ) {
+	const domDoc = domEvent.target.ownerDocument;
+	const x = domEvent.clientX;
+	const y = domEvent.clientY;
+	let domRange;
+
+	// Webkit & Blink.
+	if ( domDoc.caretRangeFromPoint && domDoc.caretRangeFromPoint( x, y ) ) {
+		domRange = domDoc.caretRangeFromPoint( x, y );
+	}
+	// FF.
+	else if ( domEvent.rangeParent ) {
+		domRange = domDoc.createRange();
+		domRange.setStart( domEvent.rangeParent, domEvent.rangeOffset );
+		domRange.collapse( true );
+	}
+
+	if ( domRange ) {
+		return doc.domConverter.domRangeToView( domRange );
+	} else {
+		return doc.selection.getFirstRange();
 	}
 }
 

+ 195 - 34
packages/ckeditor5-clipboard/tests/clipboardobserver.js

@@ -6,77 +6,238 @@
 /* globals document */
 
 import ClipboardObserver from '../src/clipboardobserver';
-import ViewDocument from '@ckeditor/ckeditor5-engine/src/view/document';
+import Document from '@ckeditor/ckeditor5-engine/src/view/document';
+import Element from '@ckeditor/ckeditor5-engine/src/view/element';
+import Range from '@ckeditor/ckeditor5-engine/src/view/range';
+import Position from '@ckeditor/ckeditor5-engine/src/view/position';
 import DataTransfer from '../src/datatransfer';
 
 describe( 'ClipboardObserver', () => {
-	let viewDocument, observer;
+	let doc, observer, root, el, range, eventSpy, preventDefaultSpy;
 
 	beforeEach( () => {
-		viewDocument = new ViewDocument();
-		observer = viewDocument.addObserver( ClipboardObserver );
+		doc = new Document();
+		root = doc.createRoot( 'div' );
+
+		// Create view and DOM structures.
+		el = new Element( 'p' );
+		root.appendChildren( el );
+		doc.domConverter.viewToDom( root, document, { withChildren: true, bind: true } );
+
+		doc.selection.collapse( el );
+		range = new Range( new Position( root, 1 ) );
+		// Just making sure that the following tests will check anything.
+		expect( range.isEqual( doc.selection.getFirstRange() ) ).to.be.false;
+
+		observer = doc.addObserver( ClipboardObserver );
+
+		eventSpy = sinon.spy();
+		preventDefaultSpy = sinon.spy();
 	} );
 
 	it( 'should define domEventType', () => {
 		expect( observer.domEventType ).to.deep.equal( [ 'paste', 'copy', 'cut', 'drop' ] );
 	} );
 
-	describe( 'onDomEvent', () => {
-		let pasteSpy, preventDefaultSpy;
+	describe( 'paste event', () => {
+		it( 'should be fired with the right event data', () => {
+			const dataTransfer = mockDomDataTransfer();
+			const targetElement = mockDomTargetElement( {} );
 
-		function getDataTransfer() {
-			return {
-				getData( type ) {
-					return 'foo:' + type;
-				}
-			};
-		}
-
-		beforeEach( () => {
-			pasteSpy = sinon.spy();
-			preventDefaultSpy = sinon.spy();
-		} );
-
-		it( 'should fire paste with the right event data - clipboardData', () => {
-			const dataTransfer = getDataTransfer();
-
-			viewDocument.on( 'paste', pasteSpy );
+			doc.on( 'paste', eventSpy );
 
 			observer.onDomEvent( {
 				type: 'paste',
-				target: document.body,
+				target: targetElement,
 				clipboardData: dataTransfer,
 				preventDefault: preventDefaultSpy
 			} );
 
-			expect( pasteSpy.calledOnce ).to.be.true;
+			expect( eventSpy.calledOnce ).to.be.true;
+
+			const data = eventSpy.args[ 0 ][ 1 ];
+
+			expect( data.domTarget ).to.equal( targetElement );
 
-			const data = pasteSpy.args[ 0 ][ 1 ];
-			expect( data.domTarget ).to.equal( document.body );
 			expect( data.dataTransfer ).to.be.instanceOf( DataTransfer );
 			expect( data.dataTransfer.getData( 'x/y' ) ).to.equal( 'foo:x/y' );
+
 			expect( preventDefaultSpy.calledOnce ).to.be.true;
 		} );
+	} );
 
-		it( 'should fire paste with the right event data - dataTransfer', () => {
-			const dataTransfer = getDataTransfer();
+	describe( 'drop event', () => {
+		it( 'should be fired with the right event data - basics', () => {
+			const dataTransfer = mockDomDataTransfer();
+			const targetElement = mockDomTargetElement( {} );
 
-			viewDocument.on( 'drop', pasteSpy );
+			doc.on( 'drop', eventSpy );
 
 			observer.onDomEvent( {
 				type: 'drop',
-				target: document.body,
+				target: targetElement,
 				dataTransfer,
 				preventDefault: preventDefaultSpy
 			} );
 
-			expect( pasteSpy.calledOnce ).to.be.true;
+			expect( eventSpy.calledOnce ).to.be.true;
+
+			const data = eventSpy.args[ 0 ][ 1 ];
+
+			expect( data.domTarget ).to.equal( targetElement );
 
-			const data = pasteSpy.args[ 0 ][ 1 ];
-			expect( data.domTarget ).to.equal( document.body );
 			expect( data.dataTransfer ).to.be.instanceOf( DataTransfer );
 			expect( data.dataTransfer.getData( 'x/y' ) ).to.equal( 'foo:x/y' );
+
+			expect( data.dropRange.isEqual( doc.selection.getFirstRange() ) ).to.be.true;
+
 			expect( preventDefaultSpy.calledOnce ).to.be.true;
 		} );
+
+		it( 'should be fired with the right event data – dropRange (when no info about it in the drop event)', () => {
+			const dataTransfer = mockDomDataTransfer();
+			const targetElement = mockDomTargetElement( {} );
+
+			doc.on( 'drop', eventSpy );
+
+			observer.onDomEvent( {
+				type: 'drop',
+				target: targetElement,
+				dataTransfer,
+				preventDefault() {}
+			} );
+
+			expect( eventSpy.calledOnce ).to.be.true;
+
+			const data = eventSpy.args[ 0 ][ 1 ];
+
+			expect( data.dropRange.isEqual( doc.selection.getFirstRange() ) ).to.be.true;
+		} );
+
+		it( 'should be fired with the right event data – dropRange (when document.caretRangeFromPoint present)', () => {
+			let caretRangeFromPointCalledWith;
+
+			const domRange = doc.domConverter.viewRangeToDom( range );
+			const dataTransfer = mockDomDataTransfer();
+			const targetElement = mockDomTargetElement( {
+				caretRangeFromPoint( x, y ) {
+					caretRangeFromPointCalledWith = [ x, y ];
+
+					return domRange;
+				}
+			} );
+
+			doc.on( 'drop', eventSpy );
+
+			observer.onDomEvent( {
+				type: 'drop',
+				target: targetElement,
+				dataTransfer,
+				clientX: 10,
+				clientY: 20,
+				preventDefault() {}
+			} );
+
+			expect( eventSpy.calledOnce ).to.be.true;
+
+			const data = eventSpy.args[ 0 ][ 1 ];
+
+			expect( data.dropRange.isEqual( range ) ).to.be.true;
+			expect( caretRangeFromPointCalledWith ).to.deep.equal( [ 10, 20 ] );
+		} );
+
+		it( 'should be fired with the right event data – dropRange (when evt.rangeParent|Offset present)', () => {
+			const domRange = doc.domConverter.viewRangeToDom( range );
+			const dataTransfer = mockDomDataTransfer();
+			const targetElement = mockDomTargetElement( {
+				createRange() {
+					return document.createRange();
+				}
+			} );
+
+			doc.on( 'drop', eventSpy );
+
+			observer.onDomEvent( {
+				type: 'drop',
+				target: targetElement,
+				dataTransfer,
+				rangeParent: domRange.startContainer,
+				rangeOffset: domRange.startOffset,
+				preventDefault() {}
+			} );
+
+			expect( eventSpy.calledOnce ).to.be.true;
+
+			const data = eventSpy.args[ 0 ][ 1 ];
+
+			expect( data.dropRange.isEqual( range ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'clipboardInput event', () => {
+		it( 'should be fired on paste', () => {
+			const dataTransfer = new DataTransfer( mockDomDataTransfer() );
+			const normalPrioritySpy = sinon.spy();
+
+			doc.on( 'clipboardInput', eventSpy );
+			doc.on( 'paste', normalPrioritySpy );
+
+			doc.fire( 'paste', {
+				dataTransfer,
+				preventDefault: preventDefaultSpy
+			} );
+
+			expect( eventSpy.calledOnce ).to.be.true;
+			expect( preventDefaultSpy.calledOnce ).to.be.true;
+
+			const data = eventSpy.args[ 0 ][ 1 ];
+			expect( data.dataTransfer ).to.equal( dataTransfer );
+
+			expect( data.targetRanges ).to.have.length( 1 );
+			expect( data.targetRanges[ 0 ].isEqual( doc.selection.getFirstRange() ) ).to.be.true;
+
+			expect( sinon.assert.callOrder( normalPrioritySpy, eventSpy ) );
+		} );
+
+		it( 'should be fired on drop', () => {
+			const dataTransfer = new DataTransfer( mockDomDataTransfer() );
+			const normalPrioritySpy = sinon.spy();
+
+			doc.on( 'clipboardInput', eventSpy );
+			doc.on( 'drop', normalPrioritySpy );
+
+			doc.fire( 'drop', {
+				dataTransfer,
+				preventDefault: preventDefaultSpy,
+				dropRange: range
+			} );
+
+			expect( eventSpy.calledOnce ).to.be.true;
+			expect( preventDefaultSpy.calledOnce ).to.be.true;
+
+			const data = eventSpy.args[ 0 ][ 1 ];
+			expect( data.dataTransfer ).to.equal( dataTransfer );
+
+			expect( data.targetRanges ).to.have.length( 1 );
+			expect( data.targetRanges[ 0 ].isEqual( range ) ).to.be.true;
+
+			expect( sinon.assert.callOrder( normalPrioritySpy, eventSpy ) );
+		} );
 	} );
 } );
+
+// Returns a super simple mock of HTMLElement (we use only ownerDocument from it).
+function mockDomTargetElement( documentMock ) {
+	return {
+		ownerDocument: documentMock
+	};
+}
+
+function mockDomDataTransfer() {
+	return {
+		files: [],
+		getData( type ) {
+			return 'foo:' + type;
+		}
+	};
+}