diff --git a/packages/ckeditor5-autosave/package.json b/packages/ckeditor5-autosave/package.json index ba3334ceab4..b9f4294ce00 100644 --- a/packages/ckeditor5-autosave/package.json +++ b/packages/ckeditor5-autosave/package.json @@ -20,6 +20,7 @@ "@ckeditor/ckeditor5-dev-utils": "^25.4.0", "@ckeditor/ckeditor5-editor-classic": "^30.0.0", "@ckeditor/ckeditor5-paragraph": "^30.0.0", + "@ckeditor/ckeditor5-source-editing": "^30.0.0", "@ckeditor/ckeditor5-theme-lark": "^30.0.0", "webpack": "^4.43.0", "webpack-cli": "^3.3.11" diff --git a/packages/ckeditor5-autosave/tests/manual/autosave.js b/packages/ckeditor5-autosave/tests/manual/autosave.js index 6b9c34e69e4..ff10010e500 100644 --- a/packages/ckeditor5-autosave/tests/manual/autosave.js +++ b/packages/ckeditor5-autosave/tests/manual/autosave.js @@ -7,13 +7,16 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting'; import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import Autosave from '../../src/autosave'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ ArticlePluginSet, Autosave ], - toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], + plugins: [ ArticlePluginSet, Autosave, SourceEditing ], + toolbar: [ + 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo', '|', 'sourceEditing' + ], image: { toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'imageTextAlternative' ] }, diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.js b/packages/ckeditor5-engine/src/controller/datacontroller.js index e509e0b8988..641709e2187 100644 --- a/packages/ckeditor5-engine/src/controller/datacontroller.js +++ b/packages/ckeditor5-engine/src/controller/datacontroller.js @@ -145,6 +145,7 @@ export default class DataController { this.decorate( 'init' ); this.decorate( 'set' ); + this.decorate( 'get' ); // Fire the `ready` event when the initialization has completed. Such low-level listener gives possibility // to plug into the initialization pipeline without interrupting the initialization flow. @@ -163,6 +164,7 @@ export default class DataController { * Returns the model's data converted by downcast dispatchers attached to {@link #downcastDispatcher} and * formatted by the {@link #processor data processor}. * + * @fires get * @param {Object} [options] Additional configuration for the retrieved data. `DataController` provides two optional * properties: `rootName` and `trim`. Other properties of this object are specified by various editor features. * @param {String} [options.rootName='main'] Root name. @@ -523,6 +525,15 @@ export default class DataController { * * @event set */ + + /** + * Event fired after {@link #get get() method} has been run. + * + * The `get` event is fired by decorated {@link #get} method. + * See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples. + * + * @event get + */ } mix( DataController, ObservableMixin ); diff --git a/packages/ckeditor5-engine/tests/controller/datacontroller.js b/packages/ckeditor5-engine/tests/controller/datacontroller.js index 3e285e4f952..45bbd3f4cfb 100644 --- a/packages/ckeditor5-engine/tests/controller/datacontroller.js +++ b/packages/ckeditor5-engine/tests/controller/datacontroller.js @@ -398,6 +398,16 @@ describe( 'DataController', () => { downcastHelpers.elementToElement( { model: 'paragraph', view: 'p' } ); } ); + it( 'should be decorated', () => { + const spy = sinon.spy(); + + data.on( 'get', spy ); + + data.get(); + + sinon.assert.calledWithExactly( spy, sinon.match.any, [] ); + } ); + it( 'should get paragraph with text', () => { setData( model, 'foo' ); diff --git a/packages/ckeditor5-source-editing/src/sourceediting.js b/packages/ckeditor5-source-editing/src/sourceediting.js index 433d3f0abf4..048506e9342 100644 --- a/packages/ckeditor5-source-editing/src/sourceediting.js +++ b/packages/ckeditor5-source-editing/src/sourceediting.js @@ -154,6 +154,13 @@ export default class SourceEditing extends Plugin { this.listenTo( editor, 'change:isReadOnly', ( evt, name, isReadOnly ) => this._handleReadOnlyMode( isReadOnly ) ); } + + // Update the editor data while calling editor.getData() in the source editing mode. + editor.data.on( 'get', () => { + if ( this.isSourceEditingMode ) { + this._updateEditorData(); + } + }, { priority: 'high' } ); } /** @@ -261,6 +268,29 @@ export default class SourceEditing extends Plugin { const editor = this.editor; const editingView = editor.editing.view; + this._updateEditorData(); + + editingView.change( writer => { + for ( const [ rootName ] of this._replacedRoots ) { + writer.removeClass( 'ck-hidden', editingView.document.getRoot( rootName ) ); + } + } ); + + this._elementReplacer.restore(); + + this._replacedRoots.clear(); + this._dataFromRoots.clear(); + + editingView.focus(); + } + + /** + * Updates the source data in all hidden editing roots. + * + * @private + */ + _updateEditorData() { + const editor = this.editor; const data = {}; for ( const [ rootName, domSourceEditingElementWrapper ] of this._replacedRoots ) { @@ -272,25 +302,11 @@ export default class SourceEditing extends Plugin { if ( oldData !== newData ) { data[ rootName ] = newData; } - - editingView.change( writer => { - const viewRoot = editingView.document.getRoot( rootName ); - - writer.removeClass( 'ck-hidden', viewRoot ); - } ); } - this._elementReplacer.restore(); - - this._replacedRoots.clear(); - - this._dataFromRoots.clear(); - if ( Object.keys( data ).length ) { editor.data.set( data, { batchType: 'default' } ); } - - editor.editing.view.focus(); } /** diff --git a/packages/ckeditor5-source-editing/tests/sourceediting.js b/packages/ckeditor5-source-editing/tests/sourceediting.js index e85e334f874..815e5b81468 100644 --- a/packages/ckeditor5-source-editing/tests/sourceediting.js +++ b/packages/ckeditor5-source-editing/tests/sourceediting.js @@ -453,6 +453,40 @@ describe( 'SourceEditing', () => { expect( editor.data.get() ).to.equal( '

Foo

' ); } ); + it( 'should update the editor data after calling editor.getData() in the source editing mode', () => { + const setDataSpy = sinon.spy(); + + editor.data.on( 'set', setDataSpy ); + + button.fire( 'execute' ); + + const domRoot = editor.editing.view.getDomRoot(); + const textarea = domRoot.nextSibling.children[ 0 ]; + + textarea.value = 'foo'; + textarea.dispatchEvent( new Event( 'input' ) ); + + // Trigger getData() while in the source editing mode. + expect( editor.getData() ).to.equal( '

foo

' ); + + textarea.value = 'bar'; + textarea.dispatchEvent( new Event( 'input' ) ); + + // Exit source editing mode. + button.fire( 'execute' ); + + expect( setDataSpy.calledTwice ).to.be.true; + expect( setDataSpy.firstCall.args[ 1 ] ).to.deep.equal( [ + { main: 'foo' }, + { batchType: 'default' } + ] ); + expect( setDataSpy.secondCall.args[ 1 ] ).to.deep.equal( [ + { main: 'bar' }, + { batchType: 'default' } + ] ); + expect( editor.data.get() ).to.equal( '

bar

' ); + } ); + it( 'should insert the formatted HTML source (editor output) into the textarea', () => { button.fire( 'execute' );