8
0
فهرست منبع

Merge pull request #30 from ckeditor/t/29

Feature: Added dropRange to the drop event and targetRanges to the clipboardInput event. Closes #29.
Piotr Jasiun 8 سال پیش
والد
کامیت
b65d2615e9

+ 1 - 5
packages/ckeditor5-clipboard/package.json

@@ -13,12 +13,8 @@
     "@ckeditor/ckeditor5-block-quote": "^0.1.1",
     "@ckeditor/ckeditor5-editor-classic": "^0.7.3",
     "@ckeditor/ckeditor5-paragraph": "^0.8.0",
-    "@ckeditor/ckeditor5-enter": "^0.9.1",
-    "@ckeditor/ckeditor5-heading": "^0.9.1",
+    "@ckeditor/ckeditor5-presets": "^0.2.2",
     "@ckeditor/ckeditor5-link": "^0.7.0",
-    "@ckeditor/ckeditor5-list": "^0.6.1",
-    "@ckeditor/ckeditor5-typing": "^0.9.1",
-    "@ckeditor/ckeditor5-undo": "^0.8.1",
     "eslint-config-ckeditor5": "^1.0.0",
     "gulp": "^3.9.1",
     "guppy-pre-commit": "^0.4.0"

+ 53 - 8
packages/ckeditor5-clipboard/src/clipboardobserver.js

@@ -30,28 +30,72 @@ 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();
 	}
 }
 
 /**
- * Fired with a `dataTransfer` which comes from the clipboard (was {@link module:engine/view/document~Document#event:paste pasted}
- * or {@link module:engine/view/document~Document#event:drop dropped}) and
- * should be processed in order to be inserted into the editor.
+ * Fired as a continuation of {@link #event:paste} and {@link #event:drop} events.
  * It's part of the {@link module:clipboard/clipboard~Clipboard "clipboard pipeline"}.
  *
+ * Fired with a `dataTransfer` which comes from the clipboard and which content should be processed
+ * and inserted into the editor.
+ *
+ * Note that this event is not available by default. To make it available {@link module:clipboard/clipboardobserver~ClipboardObserver}
+ * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/document~Document#addObserver} method.
+ * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
+ *
  * @see module:clipboard/clipboardobserver~ClipboardObserver
  * @see module:clipboard/clipboard~Clipboard
  * @event module:engine/view/document~Document#event:clipboardInput
  * @param {Object} data Event data.
  * @param {module:clipboard/datatransfer~DataTransfer} data.dataTransfer Data transfer instance.
+ * @param {Array.<module:engine/view/range~Range>} data.targetRanges Ranges which are the target of the operation
+ * (usually – into which the content should be inserted).
+ * If clipboard input was triggered by a paste operation, then these are the selection ranges. If by a drop operation,
+ * then it's the drop position (which can be different than the selection at the moment of drop).
  */
 
 /**
@@ -63,9 +107,10 @@ export default class ClipboardObserver extends DomEventObserver {
  * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/document~Document#addObserver} method.
  * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
  *
- * @see module:clipboard/clipboardobserver~ClipboardObserver
+ * @see module:engine/view/document~Document#event:clipboardInput
  * @event module:engine/view/document~Document#event:drop
  * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
+ * @param {module:engine/view/range~Range} dropRange The position into which the content is dropped.
  */
 
 /**
@@ -77,7 +122,7 @@ export default class ClipboardObserver extends DomEventObserver {
  * needs to be added to {@link module:engine/view/document~Document} by the {@link module:engine/view/document~Document#addObserver} method.
  * It's done by the {@link module:clipboard/clipboard~Clipboard} feature. If it's not loaded, it must be done manually.
  *
- * @see module:clipboard/clipboardobserver~ClipboardObserver
+ * @see module:engine/view/document~Document#event:clipboardInput
  * @event module:engine/view/document~Document#event:paste
  * @param {module:clipboard/clipboardobserver~ClipboardEventData} data Event data.
  */

+ 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;
+		}
+	};
+}

+ 2 - 22
packages/ckeditor5-clipboard/tests/manual/copycut.js

@@ -6,32 +6,12 @@
 /* globals console, window, document */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-import Typing from '@ckeditor/ckeditor5-typing/src/typing';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import Undo from '@ckeditor/ckeditor5-undo/src/undo';
-import Enter from '@ckeditor/ckeditor5-enter/src/enter';
-import Clipboard from '../../src/clipboard';
-import Link from '@ckeditor/ckeditor5-link/src/link';
-import List from '@ckeditor/ckeditor5-list/src/list';
-import Heading from '@ckeditor/ckeditor5-heading/src/heading';
-import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
-import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+import ArticlePreset from '@ckeditor/ckeditor5-presets/src/article';
 
 import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 ClassicEditor.create( document.querySelector( '#editor' ), {
-	plugins: [
-		Typing,
-		Paragraph,
-		Undo,
-		Enter,
-		Clipboard,
-		Link,
-		List,
-		Heading,
-		Bold,
-		Italic
-	],
+	plugins: [ ArticlePreset ],
 	toolbar: [ 'headings', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'undo', 'redo' ]
 } )
 .then( editor => {

+ 39 - 0
packages/ckeditor5-clipboard/tests/manual/dropping.html

@@ -0,0 +1,39 @@
+<div id="editor">
+	<h2>About CKEditor&nbsp;5, v0.3.0</h2>
+
+	<p>This is the <a href="http://ckeditor.com/blog/Third-Developer-Preview-of-CKEditor-5-Available">third developer preview</a> of <a href="https://ckeditor5.github.io">CKEditor&nbsp;5</a>.</p>
+
+	<p>After 2 years of work, building the next generation editor from scratch and closing over 670 tickets, we created a highly <strong>extensible and flexible architecture</strong> which consists of an <strong>amazing editing framework</strong> and <strong>editing solutions</strong> that will be built on top of it.</p>
+
+	<h3>Notes</h3>
+
+	<p><a href="https://ckeditor5.github.io">CKEditor&nbsp;5</a> is <i>under heavy development</i> and this demo is not production-ready software. For example:</p>
+
+	<ul>
+		<li><strong>only Chrome, Opera and Safari are supported</strong>,</li>
+		<li>Firefox requires enabling the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/onselectionchange">&ldquo;dom.select_events.enabled&rdquo;</a> option,</li>
+		<li><a href="https://github.com/ckeditor/ckeditor5/issues/342">support for pasting</a> is under development.</li>
+	</ul>
+
+	<p>It has <em>bugs</em> that we are aware of – and that we will be working on in the next few iterations of the project. Stay tuned for some updates soon!</p>
+</div>
+
+<h2 style="margin-top: 100px">Some rich content to copy</h2>
+
+<p>Copy also this content to check how pasting from outside of the editor works. Feel free to also use content from other websites.</p>
+
+<p>This is the <a href="http://ckeditor.com/blog/Third-Developer-Preview-of-CKEditor-5-Available">third developer preview</a> of <strong>CKEditor&nbsp;5</strong>.</p>
+
+<p>After 2 years of work, building the next generation editor from scratch and closing over 670 tickets, we created a highly <strong>extensible and flexible architecture</strong> which consists of an <strong>amazing editing framework</strong> and <strong>editing solutions</strong> that will be built on top of it.</p>
+
+<h3>Notes</h3>
+
+<p><a href="https://ckeditor5.github.io">CKEditor&nbsp;5</a> is <i>under heavy development</i> and this demo is not production-ready software. For example:</p>
+
+<ul>
+	<li><strong>only Chrome, Opera and Safari are supported</strong>,</li>
+	<li>Firefox requires enabling the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/onselectionchange">&ldquo;dom.select_events.enabled&rdquo;</a> option,</li>
+	<li><a href="https://github.com/ckeditor/ckeditor5/issues/342">support for pasting</a> is under development.</li>
+</ul>
+
+<p>It has <em>bugs</em> that we are aware of – and that we will be working on in the next few iterations of the project. Stay tuned for some updates soon!</p>

+ 50 - 0
packages/ckeditor5-clipboard/tests/manual/dropping.js

@@ -0,0 +1,50 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, window, document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePreset from '@ckeditor/ckeditor5-presets/src/article';
+
+import Text from '@ckeditor/ckeditor5-engine/src/model/text';
+import Selection from '@ckeditor/ckeditor5-engine/src/model/selection';
+
+// import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	plugins: [ ArticlePreset ],
+	toolbar: [ 'headings', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'undo', 'redo' ]
+} )
+.then( editor => {
+	window.editor = editor;
+	// const clipboard = editor.plugins.get( 'Clipboard' );
+
+	editor.editing.view.on( 'drop', ( evt, data ) => {
+		console.clear();
+
+		console.log( '----- drop -----' );
+		console.log( data );
+		console.log( 'text/html\n', data.dataTransfer.getData( 'text/html' ) );
+		console.log( 'text/plain\n', data.dataTransfer.getData( 'text/plain' ) );
+
+		data.preventDefault();
+		evt.stop();
+
+		editor.document.enqueueChanges( () => {
+			const insertAtSelection = new Selection( [ editor.editing.mapper.toModelRange( data.dropRange ) ] );
+			editor.data.insertContent( new Text( '@' ), insertAtSelection );
+			editor.document.selection.setTo( insertAtSelection );
+		} );
+	} );
+
+	// Waiting until a real dropping support...
+	// clipboard.on( 'inputTransformation', ( evt, data ) => {
+	// 	console.log( '----- clipboardInput -----' );
+	// 	console.log( 'stringify( data.dataTransfer )\n', stringifyView( data.content ) );
+	// } );
+} )
+.catch( err => {
+	console.error( err.stack );
+} );

+ 11 - 0
packages/ckeditor5-clipboard/tests/manual/dropping.md

@@ -0,0 +1,11 @@
+## Dropping
+
+**Note:** There's no real drag&drop support yet. This test is only supposed to check if the drop position is calculated correctly.
+
+Expected: At the precise drop position (where you see the caret before releasing the mouse button) a "@" should be inserted.
+
+Notes:
+
+* It may all not work in Edge (because it's focused on file support now).
+* Drop position is not the same as selection before drop (which is not that aprarent if you drop something from outside but it's obvious if you d&d content within the editor).
+* It's a known bug that after dropping content from outside the editor there's no focus in the editor.

+ 2 - 22
packages/ckeditor5-clipboard/tests/manual/pasting.js

@@ -6,32 +6,12 @@
 /* globals console, window, document */
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
-import Typing from '@ckeditor/ckeditor5-typing/src/typing';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
-import Undo from '@ckeditor/ckeditor5-undo/src/undo';
-import Enter from '@ckeditor/ckeditor5-enter/src/enter';
-import Clipboard from '../../src/clipboard';
-import Link from '@ckeditor/ckeditor5-link/src/link';
-import List from '@ckeditor/ckeditor5-list/src/list';
-import Heading from '@ckeditor/ckeditor5-heading/src/heading';
-import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
-import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
+import ArticlePreset from '@ckeditor/ckeditor5-presets/src/article';
 
 import { stringify as stringifyView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
 
 ClassicEditor.create( document.querySelector( '#editor' ), {
-	plugins: [
-		Typing,
-		Paragraph,
-		Undo,
-		Enter,
-		Clipboard,
-		Link,
-		List,
-		Heading,
-		Bold,
-		Italic
-	],
+	plugins: [ ArticlePreset ],
 	toolbar: [ 'headings', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'undo', 'redo' ]
 } )
 .then( editor => {