From 516db236556e2cdbe3af49a107111687a890f47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 14 Jul 2017 22:53:21 +0200 Subject: [PATCH 1/4] Introduced target ranges in drop and clipboardInput events. --- src/clipboardobserver.js | 41 ++++++- tests/clipboardobserver.js | 229 +++++++++++++++++++++++++++++++------ 2 files changed, 233 insertions(+), 37 deletions(-) diff --git a/src/clipboardobserver.js b/src/clipboardobserver.js index 5dbeb70..bd91477 100644 --- a/src/clipboardobserver.js +++ b/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(); } } diff --git a/tests/clipboardobserver.js b/tests/clipboardobserver.js index dcb041a..6ae155d 100644 --- a/tests/clipboardobserver.js +++ b/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; + } + }; +} From 8e6516c734227a05d1d9affe6deb8f3b919fc2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 14 Jul 2017 23:12:48 +0200 Subject: [PATCH 2/4] Completed API docs. --- src/clipboardobserver.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/clipboardobserver.js b/src/clipboardobserver.js index bd91477..661db36 100644 --- a/src/clipboardobserver.js +++ b/src/clipboardobserver.js @@ -77,16 +77,25 @@ function getDropViewRange( doc, domEvent ) { } /** - * 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} amd {@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.} 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). */ /** @@ -98,9 +107,10 @@ function getDropViewRange( doc, domEvent ) { * 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. */ /** @@ -112,7 +122,7 @@ function getDropViewRange( doc, domEvent ) { * 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. */ From 9a484183cef270b9281dab57544df0665d273c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Fri, 14 Jul 2017 23:51:19 +0200 Subject: [PATCH 3/4] Added manual tests. --- package.json | 6 +---- tests/manual/copycut.js | 24 ++---------------- tests/manual/dropping.html | 39 +++++++++++++++++++++++++++++ tests/manual/dropping.js | 50 ++++++++++++++++++++++++++++++++++++++ tests/manual/dropping.md | 11 +++++++++ tests/manual/pasting.js | 24 ++---------------- 6 files changed, 105 insertions(+), 49 deletions(-) create mode 100644 tests/manual/dropping.html create mode 100644 tests/manual/dropping.js create mode 100644 tests/manual/dropping.md diff --git a/package.json b/package.json index cb0869e..4f352e6 100644 --- a/package.json +++ b/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" diff --git a/tests/manual/copycut.js b/tests/manual/copycut.js index 97b4538..81cbac2 100644 --- a/tests/manual/copycut.js +++ b/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 => { diff --git a/tests/manual/dropping.html b/tests/manual/dropping.html new file mode 100644 index 0000000..4583c8a --- /dev/null +++ b/tests/manual/dropping.html @@ -0,0 +1,39 @@ +
+

About CKEditor 5, v0.3.0

+ +

This is the third developer preview of CKEditor 5.

+ +

After 2 years of work, building the next generation editor from scratch and closing over 670 tickets, we created a highly extensible and flexible architecture which consists of an amazing editing framework and editing solutions that will be built on top of it.

+ +

Notes

+ +

CKEditor 5 is under heavy development and this demo is not production-ready software. For example:

+ + + +

It has bugs 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!

+
+ +

Some rich content to copy

+ +

Copy also this content to check how pasting from outside of the editor works. Feel free to also use content from other websites.

+ +

This is the third developer preview of CKEditor 5.

+ +

After 2 years of work, building the next generation editor from scratch and closing over 670 tickets, we created a highly extensible and flexible architecture which consists of an amazing editing framework and editing solutions that will be built on top of it.

+ +

Notes

+ +

CKEditor 5 is under heavy development and this demo is not production-ready software. For example:

+ + + +

It has bugs 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!

diff --git a/tests/manual/dropping.js b/tests/manual/dropping.js new file mode 100644 index 0000000..9008d22 --- /dev/null +++ b/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 ); +} ); diff --git a/tests/manual/dropping.md b/tests/manual/dropping.md new file mode 100644 index 0000000..169d9bf --- /dev/null +++ b/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. diff --git a/tests/manual/pasting.js b/tests/manual/pasting.js index b36eb89..25aac06 100644 --- a/tests/manual/pasting.js +++ b/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 => { From a02bb415d8a2d5d62350a8c686503aa73c40b339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotrek=20Koszuli=C5=84ski?= Date: Mon, 24 Jul 2017 17:15:53 +0200 Subject: [PATCH 4/4] Fixed typo. --- src/clipboardobserver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clipboardobserver.js b/src/clipboardobserver.js index 661db36..334e9a6 100644 --- a/src/clipboardobserver.js +++ b/src/clipboardobserver.js @@ -77,7 +77,7 @@ function getDropViewRange( doc, domEvent ) { } /** - * Fired as a continuation of {@link #event:paste} amd {@link #event:drop} events. + * 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