Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit e8fd17d

Browse files
author
Piotr Jasiun
authored
Merge pull request #1025 from ckeditor/t/1024
Feature: Hide caret when an editor is read-only. EditingControler is observable from now. Observable property isReadOnly is added to the ViewDocument and EditingController. Closes #1024. Closes ckeditor/ckeditor5#503.
2 parents 17e70c3 + 81bbca1 commit e8fd17d

File tree

10 files changed

+128
-32
lines changed

10 files changed

+128
-32
lines changed

src/controller/editingcontroller.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ import {
2222
clearFakeSelection
2323
} from '../conversion/model-selection-to-view-converters';
2424

25-
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
25+
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
26+
import mix from '@ckeditor/ckeditor5-utils/src/mix';
2627

2728
/**
2829
* Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering,
2930
* including selection handling. It also creates {@link ~EditingController#view view document} which build a
3031
* browser-independent virtualization over the DOM elements. Editing controller also attach default converters.
32+
*
33+
* @mixes module:utils/observablemixin~ObservableMixin
3134
*/
3235
export default class EditingController {
3336
/**
@@ -60,6 +63,19 @@ export default class EditingController {
6063
*/
6164
this.mapper = new Mapper();
6265

66+
/**
67+
* Defines whether controller is in read-only mode.
68+
*
69+
* When controller is read-ony then {module:engine/view/document~Document view document} is read-only as well.
70+
*
71+
* @observable
72+
* @member {Boolean} #isReadOnly
73+
*/
74+
this.set( 'isReadOnly', false );
75+
76+
// When controller is read-only the view document is read-only as well.
77+
this.view.bind( 'isReadOnly' ).to( this );
78+
6379
/**
6480
* Model to view conversion dispatcher, which converts changes from the model to
6581
* {@link #view editing view}.
@@ -80,39 +96,30 @@ export default class EditingController {
8096
viewSelection: this.view.selection
8197
} );
8298

83-
/**
84-
* Property keeping all listenters attached by controller on other objects, so it can
85-
* stop listening on {@link #destroy}.
86-
*
87-
* @private
88-
* @member {utils.EmitterMixin} #_listener
89-
*/
90-
this._listener = Object.create( EmitterMixin );
91-
9299
// Convert changes in model to view.
93-
this._listener.listenTo( this.model, 'change', ( evt, type, changes ) => {
100+
this.listenTo( this.model, 'change', ( evt, type, changes ) => {
94101
this.modelToView.convertChange( type, changes );
95102
}, { priority: 'low' } );
96103

97104
// Convert model selection to view.
98-
this._listener.listenTo( this.model, 'changesDone', () => {
105+
this.listenTo( this.model, 'changesDone', () => {
99106
const selection = this.model.selection;
100107

101108
this.modelToView.convertSelection( selection );
102109
this.view.render();
103110
}, { priority: 'low' } );
104111

105112
// Convert model markers changes.
106-
this._listener.listenTo( this.model.markers, 'add', ( evt, marker ) => {
113+
this.listenTo( this.model.markers, 'add', ( evt, marker ) => {
107114
this.modelToView.convertMarker( 'addMarker', marker.name, marker.getRange() );
108115
} );
109116

110-
this._listener.listenTo( this.model.markers, 'remove', ( evt, marker ) => {
117+
this.listenTo( this.model.markers, 'remove', ( evt, marker ) => {
111118
this.modelToView.convertMarker( 'removeMarker', marker.name, marker.getRange() );
112119
} );
113120

114121
// Convert view selection to model.
115-
this._listener.listenTo( this.view, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
122+
this.listenTo( this.view, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
116123

117124
// Attach default content converters.
118125
this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
@@ -158,6 +165,8 @@ export default class EditingController {
158165
*/
159166
destroy() {
160167
this.view.destroy();
161-
this._listener.stopListening();
168+
this.stopListening();
162169
}
163170
}
171+
172+
mix( EditingController, ObservableMixin );

src/view/document.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ export default class Document {
7979
*/
8080
this.roots = new Map();
8181

82+
/**
83+
* Defines whether document is in read-only mode.
84+
*
85+
* When document is read-ony then all roots are read-only as well and caret placed inside this root is hidden.
86+
*
87+
* @observable
88+
* @member {Boolean} #isReadOnly
89+
*/
90+
this.set( 'isReadOnly', false );
91+
8292
/**
8393
* True if document is focused.
8494
*
@@ -98,7 +108,7 @@ export default class Document {
98108
* @member {module:engine/view/renderer~Renderer} module:engine/view/document~Document#renderer
99109
*/
100110
this.renderer = new Renderer( this.domConverter, this.selection );
101-
this.renderer.bind( 'isFocused' ).to( this, 'isFocused' );
111+
this.renderer.bind( 'isFocused' ).to( this );
102112

103113
/**
104114
* Map of registered {@link module:engine/view/observer/observer~Observer observers}.

src/view/editableelement.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ const documentSymbol = Symbol( 'document' );
1818
* Editable element which can be a {@link module:engine/view/rooteditableelement~RootEditableElement root}
1919
* or nested editable area in the editor.
2020
*
21+
* Editable is automatically read-only when its {module:engine/view/document~Document Document} is read-only.
22+
*
2123
* @extends module:engine/view/containerelement~ContainerElement
22-
* @mixes module:utils/observablemixin~ObservaleMixin
24+
* @mixes module:utils/observablemixin~ObservableMixin
2325
*/
2426
export default class EditableElement extends ContainerElement {
2527
/**
@@ -74,6 +76,8 @@ export default class EditableElement extends ContainerElement {
7476

7577
this.setCustomProperty( documentSymbol, document );
7678

79+
this.bind( 'isReadOnly' ).to( document );
80+
7781
this.bind( 'isFocused' ).to(
7882
document,
7983
'isFocused',

src/view/observer/selectionobserver.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,10 @@ export default class SelectionObserver extends Observer {
133133
* @param {Document} domDocument DOM document.
134134
*/
135135
_handleSelectionChange( domDocument ) {
136-
if ( !this.isEnabled || !this.document.isFocused ) {
136+
// Selection is handled when document is not focused but is read-only. This is because in read-only
137+
// mode contenteditable is set as false and editor won't receive focus but we still need to know
138+
// selection position.
139+
if ( !this.isEnabled || ( !this.document.isFocused && !this.document.isReadOnly ) ) {
137140
return;
138141
}
139142

tests/controller/editingcontroller.js

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,48 @@ import { getData as getViewData } from '../../src/dev-utils/view';
2929

3030
describe( 'EditingController', () => {
3131
describe( 'constructor()', () => {
32-
it( 'should create controller with properties', () => {
33-
const model = new ModelDocument();
34-
const editing = new EditingController( model );
32+
let model, editing;
33+
34+
beforeEach( () => {
35+
model = new ModelDocument();
36+
editing = new EditingController( model );
37+
} );
38+
39+
afterEach( () => {
40+
editing.destroy();
41+
} );
3542

43+
it( 'should create controller with properties', () => {
3644
expect( editing ).to.have.property( 'model' ).that.equals( model );
3745
expect( editing ).to.have.property( 'view' ).that.is.instanceof( ViewDocument );
3846
expect( editing ).to.have.property( 'mapper' ).that.is.instanceof( Mapper );
3947
expect( editing ).to.have.property( 'modelToView' ).that.is.instanceof( ModelConversionDispatcher );
48+
expect( editing ).to.have.property( 'isReadOnly' ).that.is.false;
4049

4150
editing.destroy();
4251
} );
52+
53+
it( 'should be observable', () => {
54+
const spy = sinon.spy();
55+
56+
editing.on( 'change:foo', spy );
57+
editing.set( 'foo', 'bar' );
58+
59+
sinon.assert.calledOnce( spy );
60+
} );
61+
62+
it( 'should bind view#isReadOnly to controller#isReadOnly', () => {
63+
editing.isReadOnly = false;
64+
65+
expect( editing.view.isReadOnly ).to.false;
66+
67+
editing.isReadOnly = true;
68+
69+
expect( editing.view.isReadOnly ).to.true;
70+
} );
4371
} );
4472

45-
describe( 'createRoot', () => {
73+
describe( 'createRoot()', () => {
4674
let model, modelRoot, editing;
4775

4876
beforeEach( () => {
@@ -377,7 +405,7 @@ describe( 'EditingController', () => {
377405
} );
378406
} );
379407

380-
describe( 'destroy', () => {
408+
describe( 'destroy()', () => {
381409
it( 'should remove listenters', () => {
382410
const model = new ModelDocument();
383411
model.createRoot();

tests/view/_utils/createdocumentmock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Selection from '../../../src/view/selection';
1414
export default function createDocumentMock() {
1515
const doc = Object.create( ObservableMixin );
1616
doc.set( 'isFocused', false );
17+
doc.set( 'isReadOnly', false );
1718
doc.selection = new Selection();
1819

1920
return doc;

tests/view/document/document.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ describe( 'Document', () => {
7171
it( 'should create Document with all properties', () => {
7272
expect( count( viewDocument.domRoots ) ).to.equal( 0 );
7373
expect( count( viewDocument.roots ) ).to.equal( 0 );
74-
expect( viewDocument ).to.have.property( 'renderer' ).that.is.instanceOf( Renderer );
75-
expect( viewDocument ).to.have.property( 'domConverter' ).that.is.instanceOf( DomConverter );
76-
expect( viewDocument ).to.have.property( 'isFocused' ).that.is.false;
74+
expect( viewDocument ).to.have.property( 'renderer' ).to.instanceOf( Renderer );
75+
expect( viewDocument ).to.have.property( 'domConverter' ).to.instanceOf( DomConverter );
76+
expect( viewDocument ).to.have.property( 'isReadOnly' ).to.false;
77+
expect( viewDocument ).to.have.property( 'isFocused' ).to.false;
7778
} );
7879

7980
it( 'should add default observers', () => {

tests/view/editableelement.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,19 @@ describe( 'EditableElement', () => {
151151

152152
expect( isReadOnlySpy.calledOnce ).to.be.true;
153153
} );
154+
155+
it( 'should be bound to the document#isReadOnly', () => {
156+
const root = new RootEditableElement( 'div' );
157+
root.document = createDocumentMock();
158+
159+
root.document.isReadOnly = false;
160+
161+
expect( root.isReadOnly ).to.false;
162+
163+
root.document.isReadOnly = true;
164+
165+
expect( root.isReadOnly ).to.true;
166+
} );
154167
} );
155168

156169
describe( 'getDocument', () => {

tests/view/observer/selectionobserver.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,26 @@ describe( 'SelectionObserver', () => {
134134
changeDomSelection();
135135
} );
136136

137+
it( 'should fired if there is no focus but document is read-only', done => {
138+
const spy = sinon.spy();
139+
140+
viewDocument.isFocused = false;
141+
viewDocument.isReadOnly = true;
142+
143+
// changeDomSelection() may focus the editable element (happens on Chrome)
144+
// so cancel this because it sets the isFocused flag.
145+
viewDocument.on( 'focus', evt => evt.stop(), { priority: 'highest' } );
146+
147+
viewDocument.on( 'selectionChange', spy );
148+
149+
setTimeout( () => {
150+
sinon.assert.calledOnce( spy );
151+
done();
152+
}, 70 );
153+
154+
changeDomSelection();
155+
} );
156+
137157
it( 'should warn and not enter infinite loop', () => {
138158
// Selectionchange event is called twice per `changeDomSelection()` execution.
139159
let counter = 35;

tests/view/utils-tests/createdocumentmock.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,26 @@
66
import createDocumentMock from '../../../tests/view/_utils/createdocumentmock';
77

88
describe( 'createDocumentMock', () => {
9-
it( 'should create document mock', done => {
9+
it( 'should create document mock', () => {
1010
const docMock = createDocumentMock();
1111
const rootMock = {};
1212

13+
const isFocusedSpy = sinon.spy();
14+
const isReadOnlySpy = sinon.spy();
15+
1316
docMock.on( 'change:selectedEditable', ( evt, key, value ) => {
1417
expect( value ).to.equal( rootMock );
1518
} );
1619

17-
docMock.on( 'change:isFocused', ( evt, key, value ) => {
18-
expect( value ).to.be.true;
19-
done();
20-
} );
20+
docMock.on( 'change:isFocused', isFocusedSpy );
21+
docMock.on( 'change:isReadOnly', isReadOnlySpy );
2122

2223
docMock.isFocused = true;
24+
docMock.isReadOnly = true;
25+
26+
sinon.assert.calledOnce( isFocusedSpy );
27+
expect( isFocusedSpy.lastCall.args[ 2 ] ).to.true;
28+
sinon.assert.calledOnce( isReadOnlySpy );
29+
expect( isReadOnlySpy.lastCall.args[ 2 ] ).to.true;
2330
} );
2431
} );

0 commit comments

Comments
 (0)