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;