diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec1475b..e8116377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # CKEditor 4 Angular Integration Changelog +## ckeditor4-angular 1.2.0 + +New Features: + +* [#7](/~https://github.com/ckeditor/ckeditor4-angular/issues/7): CKEditor 4 component now exposes more CKEditor 4 native events. Thanks to [Eduard Zintz](/~https://github.com/ezintz)! New exposed events are: + * [paste](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-paste) + * [afterPaste](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-afterPaste) + * [dragStat](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragstart) + * [dragEnd](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragend) + * [drop](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-drop) + * [fileUploadRequest](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadRequest) + * [fileUploadResponse](https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadResponse) + ## ckeditor4-angular 1.1.0 Other Changes: diff --git a/src/app/simple-usage/simple-usage.component.html b/src/app/simple-usage/simple-usage.component.html index 5d451c18..fb67689a 100644 --- a/src/app/simple-usage/simple-usage.component.html +++ b/src/app/simple-usage/simple-usage.component.html @@ -14,8 +14,15 @@

{{ editor }} Editor:

(ready)="onReady( $event, editor )" (change)="onChange( $event, editor )" (focus)="onFocus( $event, editor )" - (blur)="onBlur( $event, editor )"> - + (blur)="onBlur( $event, editor )" + (paste)="onPaste( $event, editor )" + (afterPaste)="onAfterPaste( $event, editor )" + (dragStart)="onDragStart( $event, editor )" + (dragEnd)="onDragEnd( $event, editor )" + (drop)="onDrop( $event, editor )" + (fileUploadRequest)="onFileUploadRequest( $event, editor )" + (fileUploadResponse)="onFileUploadResponse( $event, editor )" + >

Note: editors have it's data synchronized, so every change in one of editors propagates to another.

diff --git a/src/app/simple-usage/simple-usage.component.ts b/src/app/simple-usage/simple-usage.component.ts index 9394ca5c..da67d3c1 100644 --- a/src/app/simple-usage/simple-usage.component.ts +++ b/src/app/simple-usage/simple-usage.component.ts @@ -46,4 +46,32 @@ You learn to appreciate each and every single one of the differences while you b onBlur( event: CKEditor4.EventInfo, editorName: string ): void { console.log( `Blurred ${editorName.toLowerCase()} editing view.` ); } + + onPaste( event: CKEditor4.EventInfo, editorName: string ): void { + console.log( `Pasted into ${editorName.toLowerCase()} editing view.` ); + } + + onAfterPaste( event: CKEditor4.EventInfo, editorName: string ): void { + console.log( `After pasted fired in ${editorName.toLowerCase()} editing view.` ); + } + + onDragStart( event: CKEditor4.EventInfo, editorName: string ): void { + console.log( `Drag started in ${editorName.toLowerCase()} editing view.` ); + } + + onDragEnd( event: CKEditor4.EventInfo, editorName: string ): void { + console.log( `Drag ended in ${editorName.toLowerCase()} editing view.` ); + } + + onDrop( event: CKEditor4.EventInfo, editorName: string ): void { + console.log( `Dropped in ${editorName.toLowerCase()} editing view.` ); + } + + onFileUploadRequest( event: CKEditor4.EventInfo, editorName: string ): void { + console.log( `File upload requested in ${editorName.toLowerCase()} editor.` ); + } + + onFileUploadResponse( event: CKEditor4.EventInfo, editorName: string ): void { + console.log( `File upload responded in ${editorName.toLowerCase()} editor.` ); + } } diff --git a/src/ckeditor/ckeditor.component.spec.ts b/src/ckeditor/ckeditor.component.spec.ts index 1997c966..fbfe6182 100644 --- a/src/ckeditor/ckeditor.component.spec.ts +++ b/src/ckeditor/ckeditor.component.spec.ts @@ -5,7 +5,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CKEditorComponent } from './ckeditor.component'; -import { whenEvent, whenDataReady, setDataMultipleTimes } from '../test.tools'; +import { + fireDragEvent, + mockDropEvent, + mockPasteEvent, + setDataMultipleTimes, + whenDataReady, + whenEvent +} from '../test.tools'; import { CKEditor4 } from './ckeditor'; import EditorType = CKEditor4.EditorType; @@ -345,6 +352,123 @@ describe( 'CKEditorComponent', () => { expect( spy ).toHaveBeenCalledTimes( 1 ); } ); + + it( 'paste should emit component paste', () => { + const pasteEventMock = mockPasteEvent(); + pasteEventMock.$.clipboardData.setData( 'text/html', '

bam

' ); + + fixture.detectChanges(); + + const spy = jasmine.createSpy(); + component.paste.subscribe( spy ); + + const editable = component.instance.editable(); + editable.fire( 'paste', pasteEventMock ); + + return whenEvent( 'paste', component ).then( () => { + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( component.instance.getData() ).toEqual( '

bam

\n' ); + } ); + } ); + + it( 'afterPaste should emit component afterPaste', () => { + const pasteEventMock = mockPasteEvent(); + pasteEventMock.$.clipboardData.setData( 'text/html', '

bam

' ); + + fixture.detectChanges(); + + const spy = jasmine.createSpy(); + component.afterPaste.subscribe( spy ); + + const editable = component.instance.editable(); + editable.fire( 'paste', pasteEventMock ); + + return whenEvent( 'afterPaste', component ).then( () => { + expect( spy ).toHaveBeenCalledTimes( 1 ); + expect( component.instance.getData() ).toEqual( '

bam

\n' ); + } ); + } ); + + it( 'drag/drop events should emit component dragStart, dragEnd and drop', async done => { + fixture.detectChanges(); + + const spyDragStart = jasmine.createSpy( 'dragstart' ); + component.dragStart.subscribe( spyDragStart ); + + const spyDragEnd = jasmine.createSpy( 'dragend' ); + component.dragEnd.subscribe( spyDragEnd ); + + const spyDrop = jasmine.createSpy( 'drop' ); + component.drop.subscribe( spyDrop ); + + whenDataReady( component.instance, () => { + const dropEvent = mockDropEvent(); + const paragraph = component.instance.editable().findOne( 'p' ); + + component.instance.getSelection().selectElement( paragraph ); + + fireDragEvent( 'dragstart', component.instance, dropEvent ); + + expect( spyDragStart ).toHaveBeenCalledTimes( 1 ); + + fireDragEvent( 'dragend', component.instance, dropEvent ); + + expect( spyDragEnd ).toHaveBeenCalledTimes( 1 ); + + // There is some issue in Firefox with simulating drag-drop flow. The drop event + // is not fired making this assertion fail. Let's skip it for now. + if ( !CKEDITOR.env.gecko ) { + fireDragEvent( 'drop', component.instance, dropEvent ); + + expect( spyDrop ).toHaveBeenCalledTimes( 1 ); + } + + done(); + } ); + } ); + + it( 'fileUploadRequest should emit component fileUploadRequest', () => { + fixture.detectChanges(); + + const spy = jasmine.createSpy(); + component.fileUploadRequest.subscribe( spy ); + + const fileLoaderMock = { + fileLoader: { + file: Blob ? new Blob() : '', + fileName: 'fileName', + xhr: { + open: function() {}, + send: function() {} + } + }, + requestData: {} + }; + + component.instance.fire( 'fileUploadRequest', fileLoaderMock ); + + expect( spy ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'fileUploadResponse should emit component fileUploadResponse', () => { + fixture.detectChanges(); + + const spy = jasmine.createSpy(); + component.fileUploadResponse.subscribe( spy ); + + const data = { + fileLoader: { + xhr: { responseText: 'Not a JSON.' }, + lang: { + filetools: { responseError: 'Error' } + } + } + }; + + component.instance.fire( 'fileUploadResponse', data ); + + expect( spy ).toHaveBeenCalledTimes( 1 ); + } ); } ); describe( 'when control value accessor callbacks are set', () => { diff --git a/src/ckeditor/ckeditor.component.ts b/src/ckeditor/ckeditor.component.ts index 3b07fd36..e35ed86f 100644 --- a/src/ckeditor/ckeditor.component.ts +++ b/src/ckeditor/ckeditor.component.ts @@ -146,6 +146,41 @@ export class CKEditorComponent implements AfterViewInit, OnDestroy, ControlValue */ @Output() dataChange = new EventEmitter(); + /** + * Fires when the native drop event occurs. It corresponds with the `editor#dragstart` + * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragstart + * event. + */ + @Output() dragStart = new EventEmitter(); + + /** + * Fires when the native drop event occurs. It corresponds with the `editor#dragend` + * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-dragend + * event. + */ + @Output() dragEnd = new EventEmitter(); + + /** + * Fires when the native drop event occurs. It corresponds with the `editor#drop` + * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-drop + * event. + */ + @Output() drop = new EventEmitter(); + + /** + * Fires when the file loader response is received. It corresponds with the `editor#fileUploadResponse` + * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadResponse + * event. + */ + @Output() fileUploadResponse = new EventEmitter(); + + /** + * Fires when the file loader should send XHR. It corresponds with the `editor#fileUploadRequest` + * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-fileUploadRequest + * event. + */ + @Output() fileUploadRequest = new EventEmitter(); + /** * Fires when the editing view of the editor is focused. It corresponds with the `editor#focus` * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-focus @@ -153,6 +188,21 @@ export class CKEditorComponent implements AfterViewInit, OnDestroy, ControlValue */ @Output() focus = new EventEmitter(); + /** + * Fires after the user initiated a paste action, but before the data is inserted. + * It corresponds with the `editor#paste` + * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-paste + * event. + */ + @Output() paste = new EventEmitter(); + + /** + * Fires after the `paste` event if content was modified. It corresponds with the `editor#afterPaste` + * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-afterPaste + * event. + */ + @Output() afterPaste = new EventEmitter(); + /** * Fires when the editing view of the editor is blurred. It corresponds with the `editor#blur` * https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html#event-blur @@ -278,6 +328,48 @@ export class CKEditorComponent implements AfterViewInit, OnDestroy, ControlValue } ); } ); + editor.on( 'paste', evt => { + this.ngZone.run( () => { + this.paste.emit( evt ); + } ); + } ); + + editor.on( 'afterPaste', evt => { + this.ngZone.run( () => { + this.afterPaste.emit( evt ); + } ); + } ); + + editor.on( 'dragend', evt => { + this.ngZone.run( () => { + this.dragEnd.emit( evt ); + } ); + }); + + editor.on( 'dragstart', evt => { + this.ngZone.run( () => { + this.dragStart.emit( evt ); + } ); + } ); + + editor.on( 'drop', evt => { + this.ngZone.run( () => { + this.drop.emit( evt ); + } ); + } ); + + editor.on( 'fileUploadRequest', evt => { + this.ngZone.run( () => { + this.fileUploadRequest.emit(evt); + } ); + } ); + + editor.on( 'fileUploadResponse', evt => { + this.ngZone.run(() => { + this.fileUploadResponse.emit(evt); + } ); + } ); + editor.on( 'blur', evt => { this.ngZone.run( () => { if ( this.onTouched ) { diff --git a/src/test.tools.ts b/src/test.tools.ts index 6c89811c..27dd1850 100644 --- a/src/test.tools.ts +++ b/src/test.tools.ts @@ -1,5 +1,7 @@ import { CKEditorComponent } from './ckeditor/ckeditor.component'; +declare var CKEDITOR: any; + export function whenEvent( evtName: string, component: CKEditorComponent ) { return new Promise( res => { component[ evtName ].subscribe( res ); @@ -29,6 +31,53 @@ export function setDataMultipleTimes( editor: any, data: Array ) { } ); } +export function mockPasteEvent() { + const dataTransfer = mockNativeDataTransfer(); + let target = new CKEDITOR.dom.element( 'div' ); + + return { + $: { + ctrlKey: true, + clipboardData: ( CKEDITOR.env.ie && CKEDITOR.env.version < 16 ) ? undefined : dataTransfer + }, + preventDefault: function() {}, + getTarget: function() { + return target; + }, + setTarget: function( t: any ) { + target = t; + } + }; +} + +export function mockDropEvent() { + const dataTransfer = mockNativeDataTransfer(); + let target = new CKEDITOR.dom.element( 'div' ); + + target.isReadOnly = function() { + return false; + }; + + return { + $: { + dataTransfer: dataTransfer + }, + preventDefault: function() {}, + getTarget: function() { + return target; + }, + setTarget: function( t: any ) { + target = t; + } + }; +} + +export function fireDragEvent( eventName: string, editor: any, evt: any ) { + const dropTarget = CKEDITOR.plugins.clipboard.getDropTarget( editor ); + + dropTarget.fire( eventName, evt ); +} + function setDataHelper( editor: any, data: Array, done: Function ) { if ( data.length ) { const content: string = data.shift(); @@ -41,3 +90,32 @@ function setDataHelper( editor: any, data: Array, done: Function ) { setTimeout( done, 100 ); } } + +function mockNativeDataTransfer() { + return { + types: [], + files: [], + _data: {}, + setData: function( type, data ) { + if ( type == 'text/plain' || type == 'Text' ) { + this._data[ 'text/plain' ] = data; + this._data.Text = data; + } else { + this._data[ type ] = data; + } + + this.types.push( type ); + }, + getData: function( type ) { + return this._data[ type ]; + }, + clearData: function( type ) { + const index = CKEDITOR.tools.indexOf( this.types, type ); + + if ( index !== -1 ) { + delete this._data[ type ]; + this.types.splice( index, 1 ); + } + } + }; +}