diff --git a/sample/index.html b/sample/index.html
index 35637a5..eef8570 100644
--- a/sample/index.html
+++ b/sample/index.html
@@ -36,6 +36,8 @@
CKEditor 5 – React Component – development sample
const readOnlyButton = document.getElementById( 'readOnly' );
const simulateErrorButton = document.getElementById( 'simulateError' );
+ const SAMPLE_READ_ONLY_LOCK_ID = 'Integration Sample';
+
class App extends React.Component {
constructor( props ) {
super( props );
@@ -82,12 +84,12 @@ CKEditor 5 – React Component – development sample
} );
editor.ui.view.listenTo( readOnlyButton, 'click', () => {
- editor.isReadOnly = !editor.isReadOnly;
+ editor.enableReadOnlyMode( SAMPLE_READ_ONLY_LOCK_ID, !editor.isReadOnly );
if ( editor.isReadOnly ) {
- this.innerText = 'Switch to editable mode';
+ readOnlyButton.innerText = 'Switch to editable mode';
} else {
- this.innerText = 'Switch to read-only mode';
+ readOnlyButton.innerText = 'Switch to read-only mode';
}
} );
},
diff --git a/src/ckeditor.jsx b/src/ckeditor.jsx
index babfbcc..5ea0373 100644
--- a/src/ckeditor.jsx
+++ b/src/ckeditor.jsx
@@ -3,6 +3,8 @@
* For licensing, see LICENSE.md.
*/
+/* globals window */
+
import React from 'react';
import PropTypes from 'prop-types';
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog';
@@ -10,6 +12,8 @@ import uid from '@ckeditor/ckeditor5-utils/src/uid';
import { ContextWatchdogContext } from './ckeditorcontext.jsx';
import ContextWatchdog from '@ckeditor/ckeditor5-watchdog/src/contextwatchdog';
+const REACT_INTEGRATION_READ_ONLY_LOCK_ID = 'Lock from React integration (@ckeditor/ckeditor5-react)';
+
export default class CKEditor extends React.Component {
constructor( props ) {
super( props );
@@ -24,6 +28,21 @@ export default class CKEditor extends React.Component {
* @type {module:watchdog/watchdog~Watchdog|EditorWatchdogAdapter}
*/
this.watchdog = null;
+
+ const { CKEDITOR_VERSION } = window;
+
+ // Starting from v34.0.0, CKEditor 5 introduces a lock mechanism enabling/disabling the read-only mode.
+ // As it is a breaking change between major releases of the integration, the component requires using
+ // CKEditor 5 in version 34 or higher.
+ if ( CKEDITOR_VERSION ) {
+ const [ major ] = CKEDITOR_VERSION.split( '.' ).map( Number );
+
+ if ( major < 34 ) {
+ console.warn( 'The component requires using CKEditor 5 in version 34 or higher.' );
+ }
+ } else {
+ console.warn( 'Cannot find the "CKEDITOR_VERSION" in the "window" scope.' );
+ }
}
/**
@@ -61,7 +80,11 @@ export default class CKEditor extends React.Component {
}
if ( 'disabled' in nextProps ) {
- this.editor.isReadOnly = nextProps.disabled;
+ if ( nextProps.disabled ) {
+ this.editor.enableReadOnlyMode( REACT_INTEGRATION_READ_ONLY_LOCK_ID );
+ } else {
+ this.editor.disableReadOnlyMode( REACT_INTEGRATION_READ_ONLY_LOCK_ID );
+ }
}
return false;
@@ -134,7 +157,11 @@ export default class CKEditor extends React.Component {
return this.props.editor.create( element, config )
.then( editor => {
if ( 'disabled' in this.props ) {
- editor.isReadOnly = this.props.disabled;
+ // Switch to the read-only mode if the `[disabled]` attribute is specified.
+ /* istanbul ignore else */
+ if ( this.props.disabled ) {
+ editor.enableReadOnlyMode( REACT_INTEGRATION_READ_ONLY_LOCK_ID );
+ }
}
const modelDocument = editor.model.document;
@@ -264,7 +291,7 @@ class EditorWatchdogAdapter {
/**
* Adds an editor configuration to the context watchdog registry. Creates an instance of it.
*
- * @param {HTMLElement | string} sourceElementOrData A source element or data for the new editor.
+ * @param {HTMLElement|string} sourceElementOrData A source element or data for the new editor.
* @param {Object} config CKEditor 5 editor config.
* @returns {Promise}
*/
diff --git a/tests/_utils-tests/editor.js b/tests/_utils-tests/editor.js
index 1e7c58b..0968ad5 100644
--- a/tests/_utils-tests/editor.js
+++ b/tests/_utils-tests/editor.js
@@ -13,12 +13,50 @@ describe( 'Editor', () => {
expect( editor.model ).is.not.undefined;
expect( editor.editing ).is.not.undefined;
} );
+ } );
+
+ describe( 'enableReadOnlyMode()', () => {
+ it( 'should enable the read-only mode for given identifier', async () => {
+ const editor = await Editor.create();
+
+ expect( editor.isReadOnly ).is.false;
+
+ editor.enableReadOnlyMode( 'foo', true );
+
+ expect( editor.isReadOnly ).is.true;
+ } );
+ } );
+
+ describe( 'disableReadOnlyMode()', () => {
+ it( 'should enable the read-only mode for given lock identifier', async () => {
+ const editor = await Editor.create();
+
+ expect( editor.isReadOnly ).is.false;
- it( 'read-only mode is disabled by default', () => {
+ editor.enableReadOnlyMode( 'foo', true );
+
+ expect( editor.isReadOnly ).is.true;
+
+ editor.disableReadOnlyMode( 'foo' );
+
+ expect( editor.isReadOnly ).is.false;
+ } );
+ } );
+
+ describe( '#isReadOnly', () => {
+ it( 'should be disabled by default when creating a new instance of the editor', () => {
const editor = new Editor();
expect( editor.isReadOnly ).is.false;
} );
+
+ it( 'should throw an error when using the setter for switching to read-only mode', async () => {
+ const editor = await Editor.create();
+
+ expect( () => {
+ editor.isReadOnly = true;
+ } ).to.throw( Error, 'Cannot use this setter anymore' );
+ } );
} );
describe( 'destroy()', () => {
diff --git a/tests/_utils/editor.js b/tests/_utils/editor.js
index 1a77f6b..106e7c3 100644
--- a/tests/_utils/editor.js
+++ b/tests/_utils/editor.js
@@ -10,9 +10,12 @@
*/
export default class Editor {
constructor() {
+ this.initializeProperties();
+ }
+
+ initializeProperties() {
this.model = Editor._model;
this.editing = Editor._editing;
- this.isReadOnly = false;
this.data = {
get() {
return '';
@@ -21,6 +24,23 @@ export default class Editor {
}
};
+ this._readOnlyLocks = new Set();
+ }
+
+ get isReadOnly() {
+ return this._readOnlyLocks.size > 0;
+ }
+
+ set isReadOnly( value ) {
+ throw new Error( 'Cannot use this setter anymore' );
+ }
+
+ enableReadOnlyMode( lockId ) {
+ this._readOnlyLocks.add( lockId );
+ }
+
+ disableReadOnlyMode( lockId ) {
+ this._readOnlyLocks.delete( lockId );
}
destroy() {
diff --git a/tests/ckeditor.jsx b/tests/ckeditor.jsx
index d233a55..54823fd 100644
--- a/tests/ckeditor.jsx
+++ b/tests/ckeditor.jsx
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-/* global HTMLDivElement */
+/* global window, HTMLDivElement */
import React from 'react';
import { configure, mount } from 'enzyme';
@@ -16,9 +16,12 @@ import turnOffDefaultErrorCatching from './_utils/turnoffdefaulterrorcatching';
configure( { adapter: new Adapter() } );
describe( ' Component', () => {
- let wrapper;
+ let wrapper, CKEDITOR_VERSION;
beforeEach( () => {
+ CKEDITOR_VERSION = window.CKEDITOR_VERSION;
+
+ window.CKEDITOR_VERSION = '34.0.0';
sinon.stub( Editor._model.document, 'on' );
sinon.stub( Editor._editing.view.document, 'on' );
} );
@@ -29,14 +32,56 @@ describe( ' Component', () => {
if ( wrapper ) {
wrapper.unmount();
}
+
+ window.CKEDITOR_VERSION = CKEDITOR_VERSION;
} );
describe( 'initialization', () => {
+ it( 'should print a warning if the "window.CKEDITOR_VERSION" variable is not available', done => {
+ delete window.CKEDITOR_VERSION;
+ const warnStub = sinon.stub( console, 'warn' );
+
+ const onReady = () => {
+ expect( warnStub.callCount ).to.equal( 1 );
+ expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'Cannot find the "CKEDITOR_VERSION" in the "window" scope.' );
+ done();
+ };
+
+ wrapper = mount( );
+ } );
+
+ it( 'should print a warning if using CKEditor 5 in version lower than 34', done => {
+ window.CKEDITOR_VERSION = '30.0.0';
+ const warnStub = sinon.stub( console, 'warn' );
+
+ const onReady = () => {
+ expect( warnStub.callCount ).to.equal( 1 );
+ expect( warnStub.firstCall.args[ 0 ] ).to.equal(
+ 'The component requires using CKEditor 5 in version 34 or higher.'
+ );
+ done();
+ };
+
+ wrapper = mount( );
+ } );
+
+ it( 'should not print any warninig if using CKEditor 5 in version 34 or higher', done => {
+ window.CKEDITOR_VERSION = '34.1.0';
+ const warnStub = sinon.stub( console, 'warn' );
+
+ const onReady = () => {
+ expect( warnStub.callCount ).to.equal( 0 );
+ done();
+ };
+
+ wrapper = mount( );
+ } );
+
it( 'calls "Editor#create()" with default configuration if not specified', async () => {
sinon.stub( Editor, 'create' ).resolves( new Editor() );
await new Promise( res => {
- wrapper = mount( );
+ wrapper = mount( );
} );
expect( Editor.create.calledOnce ).to.be.true;
@@ -47,7 +92,7 @@ describe( ' Component', () => {
it( 'passes configuration object directly to the "Editor#create()" method', async () => {
sinon.stub( Editor, 'create' ).resolves( new Editor() );
- function myPlugin() { }
+ function myPlugin() {}
const editorConfig = {
plugins: [
@@ -59,7 +104,7 @@ describe( ' Component', () => {
};
await new Promise( res => {
- wrapper = mount( );
+ wrapper = mount( );
} );
expect( Editor.create.calledOnce ).to.be.true;
@@ -79,7 +124,7 @@ describe( ' Component', () => {
sinon.stub( Editor, 'create' ).resolves( new Editor() );
await new Promise( res => {
- wrapper = mount( );
+ wrapper = mount( );
} );
expect( Editor.create.firstCall.args[ 1 ].initialData ).to.equal(
@@ -91,7 +136,7 @@ describe( ' Component', () => {
sinon.stub( Editor, 'create' ).resolves( new Editor() );
await new Promise( res => {
- wrapper = mount( Hello CKEditor 5!' } } onReady={ res } /> );
+ wrapper = mount( Hello CKEditor 5!' } } onReady={ res }/> );
} );
expect( Editor.create.firstCall.args[ 1 ].initialData ).to.equal(
@@ -125,7 +170,7 @@ describe( ' Component', () => {
await new Promise( res => {
wrapper = mount( Bar'
- } } onReady={ res } /> );
+ } } onReady={ res }/> );
} );
// We must restore "console.warn" before assertions in order to see warnings if they were logged.
@@ -146,7 +191,7 @@ describe( ' Component', () => {
wrapper = mount( );
+ onReady={ res }/> );
} );
expect( editorInstance.setData.called ).to.be.false;
@@ -156,7 +201,7 @@ describe( ' Component', () => {
sinon.stub( Editor, 'create' ).resolves( new Editor() );
await new Promise( res => {
- wrapper = mount( );
+ wrapper = mount( );
} );
const component = wrapper.instance();
@@ -167,11 +212,12 @@ describe( ' Component', () => {
it( 'displays an error if something went wrong and "onError" callback was not specified', async () => {
const error = new Error( 'Something went wrong.' );
- const consoleErrorStub = sinon.stub( console, 'error' ).callsFake( () => { } );
+ const consoleErrorStub = sinon.stub( console, 'error' ).callsFake( () => {
+ } );
sinon.stub( Editor, 'create' ).rejects( error );
- wrapper = mount( );
+ wrapper = mount( );
await new Promise( res => setTimeout( res ) );
@@ -215,7 +261,7 @@ describe( ' Component', () => {
sinon.stub( Editor, 'create' ).resolves( editorInstance );
- wrapper = mount( );
+ wrapper = mount( );
const component = wrapper.instance();
let shouldComponentUpdate;
@@ -233,7 +279,7 @@ describe( ' Component', () => {
sinon.stub( Editor, 'create' ).resolves( editorInstance );
const editor = await new Promise( resolve => {
- wrapper = mount( );
+ wrapper = mount( );
} );
expect( editor ).to.equal( editorInstance );
@@ -253,7 +299,7 @@ describe( ' Component', () => {
editor={ Editor }
onChange={ onChange }
onReady={ res }
- onError={ rej } /> );
+ onError={ rej }/> );
} );
const fireChanges = modelDocument.on.firstCall.args[ 1 ];
@@ -277,7 +323,7 @@ describe( ' Component', () => {
wrapper = mount( );
+ onError={ rej }/> );
} );
wrapper.setProps( { onChange } );
@@ -365,7 +411,7 @@ describe( ' Component', () => {
sinon.stub( editorInstance, 'getData' ).returns( 'Foo.
' );
await new Promise( res => {
- wrapper = mount( );
+ wrapper = mount( );
} );
// More events are being attached to `viewDocument`.
@@ -426,7 +472,7 @@ describe( ' Component', () => {
sinon.stub( Editor, 'create' ).rejects( originalError );
const { error, details } = await new Promise( res => {
- wrapper = mount( res( { error, details } ) } /> );
+ wrapper = mount( res( { error, details } ) }/> );
} );
expect( error ).to.equal( error );
@@ -439,7 +485,7 @@ describe( ' Component', () => {
wrapper = mount( );
+ onError={ rej }/> );
} );
const error = new CKEditorError( 'foo', wrapper.instance().editor );
@@ -467,27 +513,36 @@ describe( ' Component', () => {
describe( '#disabled', () => {
it( 'switches the editor to read-only mode if [disabled={true}]', done => {
- const onReady = function( editor ) {
+ const onReady = editor => {
expect( editor.isReadOnly ).to.be.true;
done();
};
- wrapper = mount( );
+ wrapper = mount( );
} );
it( 'switches the editor to read-only mode when [disabled={true}] property was set in runtime', async () => {
await new Promise( ( res, rej ) => {
- wrapper = mount( );
+ wrapper = mount( );
} );
wrapper.setProps( { disabled: true } );
expect( wrapper.instance().editor.isReadOnly ).to.be.true;
} );
+
+ it( 'disables the read-only mode when [disabled={false}] property was set in runtime', async () => {
+ await new Promise( ( res, rej ) => {
+ wrapper = mount( );
+ } );
+
+ expect( wrapper.instance().editor.isReadOnly ).to.be.true;
+
+ wrapper.setProps( { disabled: false } );
+
+ expect( wrapper.instance().editor.isReadOnly ).to.be.false;
+ } );
} );
describe( '#id', () => {
@@ -568,7 +623,7 @@ describe( ' Component', () => {
const consoleErrorStub = sinon.stub( console, 'error' );
const onInit = sinon.spy();
- wrapper = mount( );
+ wrapper = mount( );
consoleErrorStub.restore();
@@ -591,7 +646,7 @@ describe( ' Component', () => {
wrapper = mount( );
+ onError={ rej }/> );
} );
await new Promise( res => {
@@ -618,7 +673,7 @@ describe( ' Component', () => {
wrapper = mount( );
+ onError={ rej }/> );
} );
const component = wrapper.instance();
@@ -641,7 +696,7 @@ describe( ' Component', () => {
wrapper = mount( );
+ onError={ rej }/> );
} );
const firstEditor = wrapper.instance().editor;