Skip to content

Commit

Permalink
Merge pull request #11441 from ckeditor/cke/10496
Browse files Browse the repository at this point in the history
Feature (core): Introduced locking mechanism for `Editor#isReadOnly`. Read-only mode can now be separately enabled and disabled by multiple features, which allows for a proper control without conflicts between features. Closes #10496.

Other (core): `Editor#isReadOnly` is now a read-only property.

MAJOR BREAKING CHANGE: The `Editor#isReadOnly` property is now read-only. From now, this property is controlled by `Editor#enableReadOnlyMode( lockId )` and `Editor#disableReadOnlyMode( lockId )`, which allow controlling the `Editor#isReadOnly` state by more than one feature without collisions. See the migration guide to learn more.
  • Loading branch information
scofalik authored Apr 4, 2022
2 parents a6686a4 + 8fc68fd commit b0234d9
Show file tree
Hide file tree
Showing 37 changed files with 503 additions and 120 deletions.
11 changes: 9 additions & 2 deletions docs/_snippets/features/read-only-hide-toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ ClassicEditor
window.editor = editor;

const button = document.querySelector( '#snippet-read-only-toggle-toolbar' );
let isReadOnly = false;

button.addEventListener( 'click', () => {
editor.isReadOnly = !editor.isReadOnly;
isReadOnly = !isReadOnly;

button.innerText = editor.isReadOnly ? 'Switch to editable mode' : 'Switch to read-only mode';
editor.enableReadOnlyMode( 'docs-snippet', isReadOnly );

button.textContent = isReadOnly ?
'Turn off read-only mode' :
'Turn on read-only mode';

editor.editing.view.focus();
} );

const toolbarElement = editor.ui.view.toolbar.element;
Expand Down
11 changes: 9 additions & 2 deletions docs/_snippets/features/read-only.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,18 @@ ClassicEditor
} )
.then( editor => {
const button = document.querySelector( '#snippet-read-only-toggle' );
let isReadOnly = false;

button.addEventListener( 'click', () => {
editor.isReadOnly = !editor.isReadOnly;
isReadOnly = !isReadOnly;

button.innerText = editor.isReadOnly ? 'Switch to editable mode' : 'Switch to read-only mode';
editor.enableReadOnlyMode( 'docs-snippet', isReadOnly );

button.textContent = isReadOnly ?
'Turn off read-only mode' :
'Turn on read-only mode';

editor.editing.view.focus();
} );
} )
.catch( err => {
Expand Down
34 changes: 33 additions & 1 deletion docs/updating/migration-to-34.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
category: updating
menu-title: Migration to v34.x
order: 90
modified_at: 2022-03-15
modified_at: 2022-04-12
---

# Migration to CKEditor 5 v34.0.0
Expand Down Expand Up @@ -34,3 +34,35 @@ From now on, additional plugins will be required, when the following CKEditor 5
import RealTimeCollaborativeRevisionHistory from '@ckeditor/ckeditor5-real-time-collaboration/src/realtimecollaborativerevisionhistory';
import RevisionHistory from '@ckeditor/ckeditor5-revision-history/src/revisionhistory';
```

### Changed mechanism for setting and clearing the editor read-only mode

With this update, {@link module:core/editor/editor~Editor#isReadOnly `editor.isReadOnly`} becomes a read-only property. Setting it manually is no longer permitted and will result in an error.

Changing the editor mode it now possible only via dedicated methods that introduce a lock mechanism. Thanks to that, various features that set the read-only mode will not collide and will not have to know about each other. Basically, the editor will become editable again only if all features (that earlier set it to read-only) will allow for that.

The new methods on the `Editor` class are {@link module:core/editor/editor~Editor#enableReadOnlyMode `editor.enableReadOnlyMode( lockId )`} and {@link module:core/editor/editor~Editor#disableReadOnlyMode `editor.disableReadOnlyMode( lockId )`}, which respectively enable and disable the read-only mode.

The lock mechanism makes the editor read-only if there is at least one lock. If all locks are removed, the content becomes editable again. Because each feature is responsible only for setting and removing its own lock, the features do not conflict each other. Earlier, such issues could result in the editor content being editable when it shouldn't.

```js
// ❌ Old usage:
function makeEditorReadOnly() {
editor.isReadOnly = true;
}

function makeEditorEditable() {
editor.isReadOnly = false;
}

// ✅ New usage:
const myFeatureLockId = Symbol( 'my-feature' );

function makeEditorReadOnly() {
editor.enableReadOnlyMode( myFeatureLockId );
}

function makeEditorEditable() {
editor.disableReadOnlyMode( myFeatureLockId );
}
```
8 changes: 4 additions & 4 deletions packages/ckeditor5-clipboard/tests/clipboardpipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ describe( 'ClipboardPipeline feature', () => {
const dataTransferMock = createDataTransfer( { 'text/html': '<p>x</p>', 'text/plain': 'y' } );
const spy = sinon.stub( editor.model, 'insertContent' );

editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

viewDocument.fire( 'paste', {
dataTransfer: dataTransferMock,
Expand All @@ -307,15 +307,15 @@ describe( 'ClipboardPipeline feature', () => {

viewDocument.on( 'clipboardInput', spy, { priority: 'high' } );

editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock
} );

sinon.assert.notCalled( spy );

editor.isReadOnly = false;
editor.disableReadOnlyMode( 'unit-test' );

viewDocument.fire( 'clipboardInput', {
dataTransfer: dataTransferMock
Expand Down Expand Up @@ -507,7 +507,7 @@ describe( 'ClipboardPipeline feature', () => {
const spy = sinon.spy();

setModelData( editor.model, '<paragraph>a[bc</paragraph><paragraph>de]f</paragraph>' );
editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

viewDocument.on( 'clipboardOutput', spy );

Expand Down
12 changes: 6 additions & 6 deletions packages/ckeditor5-clipboard/tests/dragdrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ describe( 'Drag and Drop', () => {
'<p>{foo}<span class="ck ck-clipboard-drop-target-position">\u2060<span></span>\u2060</span>bar</p>'
);

editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

// Dragging.

Expand All @@ -628,7 +628,7 @@ describe( 'Drag and Drop', () => {
expect( model.markers.has( 'drop-target' ) ).to.be.false;
expect( getViewData( view, { renderUIElements: true } ) ).to.equal( '<p>{foo}bar</p>' );

editor.isReadOnly = false;
editor.disableReadOnlyMode( 'unit-test' );
// Dropping.

dataTransferMock.dropEffect = 'move';
Expand Down Expand Up @@ -808,7 +808,7 @@ describe( 'Drag and Drop', () => {
const dataTransferMock = createDataTransfer();
const spyClipboardOutput = sinon.spy();

editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

viewDocument.on( 'clipboardOutput', ( event, data ) => spyClipboardOutput( data ) );

Expand Down Expand Up @@ -890,7 +890,7 @@ describe( 'Drag and Drop', () => {
'<table><tableRow><tableCell><paragraph>abc</paragraph></tableCell></tableRow></table>'
);

editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

const dataTransferMock = createDataTransfer();
const spyClipboardOutput = sinon.spy();
Expand Down Expand Up @@ -1379,7 +1379,7 @@ describe( 'Drag and Drop', () => {
it( 'should not focus the editor while dragging over disabled editor', () => {
const stubFocus = sinon.stub( view, 'focus' );

editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

viewDocument.fire( 'dragenter' );

Expand Down Expand Up @@ -1444,7 +1444,7 @@ describe( 'Drag and Drop', () => {
it( 'should not focus the editor while dragging over disabled editor', () => {
const stubFocus = sinon.stub( view, 'focus' );

editor.isReadOnly = true;
editor.enableReadOnlyMode( 'unit-test' );

viewDocument.fire( 'dragenter' );

Expand Down
10 changes: 8 additions & 2 deletions packages/ckeditor5-clipboard/tests/manual/dragdrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,16 @@ ClassicEditor
window.editor = editor;

const button = document.getElementById( 'read-only' );
let isReadOnly = false;

button.addEventListener( 'click', () => {
editor.isReadOnly = !editor.isReadOnly;
button.textContent = editor.isReadOnly ? 'Turn off read-only mode' : 'Turn on read-only mode';
isReadOnly = !isReadOnly;

editor.enableReadOnlyMode( 'manual-test', isReadOnly );

button.textContent = isReadOnly ?
'Turn off read-only mode' :
'Turn on read-only mode';

editor.editing.view.focus();
} );
Expand Down
157 changes: 142 additions & 15 deletions packages/ckeditor5-core/src/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ export default class Editor {
*/
this.t = this.locale.t;

/**
* A set of lock IDs for the {@link #isReadOnly} getter.
*
* @private
* @type {Set.<String|Symbol>}
*/
this._readOnlyLocks = new Set();

/**
* Commands registered to the editor.
*
Expand Down Expand Up @@ -142,21 +150,6 @@ export default class Editor {
this.once( 'ready', () => ( this.state = 'ready' ), { priority: 'high' } );
this.once( 'destroy', () => ( this.state = 'destroyed' ), { priority: 'high' } );

/**
* Defines whether this editor is in read-only mode.
*
* In read-only mode the editor {@link #commands commands} are disabled so it is not possible
* to modify the document by using them. Also, the editable element(s) become non-editable.
*
* In order to make the editor read-only, you can set this value directly:
*
* editor.isReadOnly = true;
*
* @observable
* @member {Boolean} #isReadOnly
*/
this.set( 'isReadOnly', false );

/**
* The editor's model.
*
Expand Down Expand Up @@ -229,6 +222,140 @@ export default class Editor {
this.keystrokes.listenTo( this.editing.view.document );
}

/**
* Defines whether the editor is in the read-only mode.
*
* In read-only mode the editor {@link #commands commands} are disabled so it is not possible
* to modify the document by using them. Also, the editable element(s) become non-editable.
*
* In order to make the editor read-only, you need to call the {@link #enableReadOnlyMode} method:
*
* editor.enableReadOnlyMode( 'feature-id' );
*
* Later, to turn off the read-only mode, call {@link #disableReadOnlyMode}:
*
* editor.disableReadOnlyMode( 'feature-id' );
*
* @readonly
* @observable
* @member {Boolean} #isReadOnly
*/
get isReadOnly() {
return this._readOnlyLocks.size > 0;
}

set isReadOnly( value ) {
/**
* `Editor#isReadOnly` does not have a setter and should be set using `Editor#enableReadOnlyMode( lockId )` and
* `Editor#disableReadOnlyMode( lockId )`.
*
* @error editor-isreadonly-has-no-setter
*/
throw new CKEditorError( 'editor-isreadonly-has-no-setter' );
}

/**
* Turns on the read-only mode in the editor.
*
* Editor can be switched to or out of the read-only mode by many features, under various circumstances. The editor supports locking
* mechanism for the read-only mode. It enables easy control over the read-only mode when many features wants to turn it on or off at
* the same time, without conflicting with each other. It guarantees that you will not make the editor editable accidentally (which
* could lead to errors).
*
* Each read-only mode request is identified by a unique id (also called "lock"). If multiple plugins requested to turn on the
* read-only mode, then, the editor will become editable only after all these plugins turn the read-only mode off (using the same ids).
*
* Note, that you cannot force the editor to disable the read-only mode if other plugins set it.
*
* After the first `enableReadOnlyMode()` call, the {@link #isReadOnly `isReadOnly` property} will be set to `true`:
*
* editor.isReadOnly; // `false`.
* editor.enableReadOnlyMode( 'my-feature-id' );
* editor.isReadOnly; // `true`.
*
* You can turn off the read-only mode ("clear the lock") using the {@link #disableReadOnlyMode `disableReadOnlyMode()`} method:
*
* editor.enableReadOnlyMode( 'my-feature-id' );
* // ...
* editor.disableReadOnlyMode( 'my-feature-id' );
* editor.isReadOnly; // `false`.
*
* All "locks" need to be removed to enable editing:
*
* editor.enableReadOnlyMode( 'my-feature-id' );
* editor.enableReadOnlyMode( 'my-other-feature-id' );
* // ...
* editor.disableReadOnlyMode( 'my-feature-id' );
* editor.isReadOnly; // `true`.
* editor.disableReadOnlyMode( 'my-other-feature-id' );
* editor.isReadOnly; // `false`.
*
* It is possible to pass an additional argument - `value` - to determine if the lock should be set or removed.
* Passing `false` works the same way as calling {@link #disableReadOnlyMode `disableReadOnlyMode()`}:
*
* // Assuming `isConnected` is an observable property in `myFeature`.
* myFeature.on( 'change:isConnected', () => {
* editor.enableReadOnlyMode( 'my-feature-id', !myFeature.isConnected );
* } );
*
* myFeature.isConnected = false; // editor.isReadOnly -> true.
* myFeature.isConnected = true; // editor.isReadOnly -> false.
*
* @param {String|Symbol} lockId A unique ID for setting the editor to the read-only state.
* @param {Boolean} [value=true] An optional value indicating whether the lock should be set or removed.
*/
enableReadOnlyMode( lockId, value = true ) {
if ( typeof lockId !== 'string' && typeof lockId !== 'symbol' ) {
/**
* The lock ID is missing or it is not a string or symbol.
*
* @error editor-read-only-lock-id-invalid
*/
throw new CKEditorError( 'editor-read-only-lock-id-invalid', null, { lockId } );
}

if ( value === false ) {
this.disableReadOnlyMode( lockId );

return;
}

if ( this._readOnlyLocks.has( lockId ) ) {
return;
}

this._readOnlyLocks.add( lockId );

if ( this._readOnlyLocks.size === 1 ) {
// Manually fire the `change:isReadOnly` event as only getter is provided.
this.fire( 'change:isReadOnly', 'isReadOnly', true, false );
}
}

/**
* Removes the read-only lock from the editor with given lock ID.
*
* When no lock is present on the editor anymore, then the {@link #isReadOnly `isReadOnly` property} will be set to `false`.
*
* @param {String|Symbol} lockId The lock ID for setting the editor to the read-only state.
*/
disableReadOnlyMode( lockId ) {
if ( typeof lockId !== 'string' && typeof lockId !== 'symbol' ) {
throw new CKEditorError( 'editor-read-only-lock-id-invalid', null, { lockId } );
}

if ( !this._readOnlyLocks.has( lockId ) ) {
return;
}

this._readOnlyLocks.delete( lockId );

if ( this._readOnlyLocks.size === 0 ) {
// Manually fire the `change:isReadOnly` event as only getter is provided.
this.fire( 'change:isReadOnly', 'isReadOnly', false, true );
}
}

/**
* Loads and initializes plugins specified in the configuration.
*
Expand Down
Loading

0 comments on commit b0234d9

Please sign in to comment.