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

Commit edd400f

Browse files
author
Piotr Jasiun
authored
Merge pull request #25 from ckeditor/t/ckeditor5/479
Feature: Enabled the decoupled editor placeholder (see ckeditor/ckeditor5#479). BREAKING CHANGE: The second argument of `DecoupledEditorUIView.constructor()` is an editing view instance now.
2 parents 02a52e6 + 0728249 commit edd400f

File tree

8 files changed

+260
-43
lines changed

8 files changed

+260
-43
lines changed

src/decouplededitor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ export default class DecoupledEditor extends Editor {
7373

7474
this.model.document.createRoot();
7575

76-
this.ui = new DecoupledEditorUI( this, new DecoupledEditorUIView( this.locale, this.sourceElement ) );
76+
const view = new DecoupledEditorUIView( this.locale, this.editing.view, this.sourceElement );
77+
this.ui = new DecoupledEditorUI( this, view );
7778
}
7879

7980
/**

src/decouplededitorui.js

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
1111
import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
1212
import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalizetoolbarconfig';
13+
import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';
1314

1415
/**
1516
* The decoupled editor UI class.
@@ -49,38 +50,100 @@ export default class DecoupledEditorUI extends EditorUI {
4950
init() {
5051
const editor = this.editor;
5152
const view = this.view;
53+
const editingView = editor.editing.view;
54+
const editable = view.editable;
55+
const editingRoot = editingView.document.getRoot();
56+
57+
// The editable UI and editing root should share the same name. Then name is used
58+
// to recognize the particular editable, for instance in ARIA attributes.
59+
view.editable.name = editingRoot.rootName;
5260

5361
view.render();
5462

55-
// Set up the editable.
56-
const editingRoot = editor.editing.view.document.getRoot();
57-
view.editable.bind( 'isReadOnly' ).to( editingRoot );
58-
view.editable.bind( 'isFocused' ).to( editor.editing.view.document );
59-
editor.editing.view.attachDomRoot( view.editable.element );
60-
view.editable.name = editingRoot.rootName;
63+
// The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
64+
// But it can be available earlier if a DOM element has been passed to DecoupledEditor.create().
65+
const editableElement = editable.element;
6166

62-
this._editableElements.set( view.editable.name, view.editable.element );
67+
// Register the editable UI view in the editor. A single editor instance can aggregate multiple
68+
// editable areas (roots) but the decoupled editor has only one.
69+
this._editableElements.set( editable.name, editableElement );
6370

64-
this.focusTracker.add( view.editable.element );
71+
// Let the global focus tracker know that the editable UI element is focusable and
72+
// belongs to the editor. From now on, the focus tracker will sustain the editor focus
73+
// as long as the editable is focused (e.g. the user is typing).
74+
this.focusTracker.add( editableElement );
6575

66-
this.view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
76+
// Let the editable UI element respond to the changes in the global editor focus
77+
// tracker. It has been added to the same tracker a few lines above but, in reality, there are
78+
// many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
79+
// as they have focus, the editable should act like it is focused too (although technically
80+
// it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
81+
// Doing otherwise will result in editable focus styles disappearing, once e.g. the
82+
// toolbar gets focused.
83+
view.editable.bind( 'isFocused' ).to( this.focusTracker );
6784

68-
enableToolbarKeyboardFocus( {
69-
origin: editor.editing.view,
70-
originFocusTracker: this.focusTracker,
71-
originKeystrokeHandler: editor.keystrokes,
72-
toolbar: this.view.toolbar
73-
} );
85+
// Bind the editable UI element to the editing view, making it an end– and entry–point
86+
// of the editor's engine. This is where the engine meets the UI.
87+
editingView.attachDomRoot( editableElement );
7488

89+
this._initPlaceholder();
90+
this._initToolbar();
7591
this.fire( 'ready' );
7692
}
7793

7894
/**
7995
* @inheritDoc
8096
*/
8197
destroy() {
82-
this.view.destroy();
98+
const view = this.view;
99+
const editingView = this.editor.editing.view;
100+
101+
editingView.detachDomRoot( view.editable.name );
102+
view.destroy();
83103

84104
super.destroy();
85105
}
106+
107+
/**
108+
* Initializes the inline editor toolbar and its panel.
109+
*
110+
* @private
111+
*/
112+
_initToolbar() {
113+
const editor = this.editor;
114+
const view = this.view;
115+
const toolbar = view.toolbar;
116+
117+
toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
118+
119+
enableToolbarKeyboardFocus( {
120+
origin: editor.editing.view,
121+
originFocusTracker: this.focusTracker,
122+
originKeystrokeHandler: editor.keystrokes,
123+
toolbar
124+
} );
125+
}
126+
127+
/**
128+
* Enable the placeholder text on the editing root, if any was configured.
129+
*
130+
* @private
131+
*/
132+
_initPlaceholder() {
133+
const editor = this.editor;
134+
const editingView = editor.editing.view;
135+
const editingRoot = editingView.document.getRoot();
136+
137+
const placeholderText = editor.config.get( 'placeholder' ) ||
138+
editor.sourceElement && editor.sourceElement.getAttribute( 'placeholder' );
139+
140+
if ( placeholderText ) {
141+
enablePlaceholder( {
142+
view: editingView,
143+
element: editingRoot,
144+
text: placeholderText,
145+
isDirectHost: false
146+
} );
147+
}
148+
}
86149
}

src/decouplededitoruiview.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ export default class DecoupledEditorUIView extends EditorUIView {
2828
* Creates an instance of the decoupled editor UI view.
2929
*
3030
* @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
31+
* @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
3132
* @param {HTMLElement} [editableElement] The editable element. If not specified, it will be automatically created by
3233
* {@link module:ui/editableui/editableuiview~EditableUIView}. Otherwise, the given element will be used.
3334
*/
34-
constructor( locale, editableElement ) {
35+
constructor( locale, editingView, editableElement ) {
3536
super( locale );
3637

3738
/**
@@ -48,7 +49,7 @@ export default class DecoupledEditorUIView extends EditorUIView {
4849
* @readonly
4950
* @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
5051
*/
51-
this.editable = new InlineEditableUIView( locale, editableElement );
52+
this.editable = new InlineEditableUIView( locale, editingView, editableElement );
5253

5354
// This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
5455
// Also because of the above, make sure the toolbar supports rounded corners.

tests/decouplededitorui.js

Lines changed: 115 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
1010
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
1111
import DecoupledEditorUI from '../src/decouplededitorui';
1212
import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
13+
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
1314
import DecoupledEditorUIView from '../src/decouplededitoruiview';
1415

1516
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
1617
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
1718
import utils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
19+
import { isElement } from 'lodash-es';
1820

1921
describe( 'DecoupledEditorUI', () => {
2022
let editor, view, ui, viewElement;
@@ -23,7 +25,7 @@ describe( 'DecoupledEditorUI', () => {
2325

2426
beforeEach( () => {
2527
return VirtualDecoupledTestEditor
26-
.create( {
28+
.create( '', {
2729
toolbar: [ 'foo', 'bar' ],
2830
} )
2931
.then( newEditor => {
@@ -69,34 +71,75 @@ describe( 'DecoupledEditorUI', () => {
6971
view.editable,
7072
{ isFocused: false },
7173
[
72-
[ editor.editing.view.document, { isFocused: true } ]
74+
[ ui.focusTracker, { isFocused: true } ]
7375
],
7476
{ isFocused: true }
7577
);
7678
} );
7779

78-
it( 'binds view.editable#isReadOnly', () => {
79-
const editable = editor.editing.view.document.getRoot();
80+
it( 'attaches editable UI as view\'s DOM root', () => {
81+
expect( editor.editing.view.getDomRoot() ).to.equal( view.editable.element );
82+
} );
83+
} );
8084

81-
utils.assertBinding(
82-
view.editable,
83-
{ isReadOnly: false },
84-
[
85-
[ editable, { isReadOnly: true } ]
86-
],
87-
{ isReadOnly: true }
88-
);
85+
describe( 'placeholder', () => {
86+
it( 'sets placeholder from editor.config.placeholder', () => {
87+
return VirtualDecoupledTestEditor
88+
.create( 'foo', {
89+
extraPlugins: [ Paragraph ],
90+
placeholder: 'placeholder-text',
91+
} )
92+
.then( newEditor => {
93+
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
94+
95+
expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
96+
97+
return newEditor.destroy();
98+
} );
8999
} );
90100

91-
it( 'attaches editable UI as view\'s DOM root', () => {
92-
expect( editor.editing.view.getDomRoot() ).to.equal( view.editable.element );
101+
it( 'sets placeholder from "placeholder" attribute of a passed element', () => {
102+
const element = document.createElement( 'div' );
103+
104+
element.setAttribute( 'placeholder', 'placeholder-text' );
105+
106+
return VirtualDecoupledTestEditor
107+
.create( element, {
108+
extraPlugins: [ Paragraph ]
109+
} )
110+
.then( newEditor => {
111+
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
112+
113+
expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
114+
115+
return newEditor.destroy();
116+
} );
117+
} );
118+
119+
it( 'uses editor.config.placeholder rather than "placeholder" attribute of a passed element', () => {
120+
const element = document.createElement( 'div' );
121+
122+
element.setAttribute( 'placeholder', 'placeholder-text' );
123+
124+
return VirtualDecoupledTestEditor
125+
.create( element, {
126+
placeholder: 'config takes precedence',
127+
extraPlugins: [ Paragraph ]
128+
} )
129+
.then( newEditor => {
130+
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
131+
132+
expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'config takes precedence' );
133+
134+
return newEditor.destroy();
135+
} );
93136
} );
94137
} );
95138

96139
describe( 'view.toolbar#items', () => {
97140
it( 'are filled with the config.toolbar (specified as an Array)', () => {
98141
return VirtualDecoupledTestEditor
99-
.create( {
142+
.create( '', {
100143
toolbar: [ 'foo', 'bar' ]
101144
} )
102145
.then( editor => {
@@ -111,7 +154,7 @@ describe( 'DecoupledEditorUI', () => {
111154

112155
it( 'are filled with the config.toolbar (specified as an Object)', () => {
113156
return VirtualDecoupledTestEditor
114-
.create( {
157+
.create( '', {
115158
toolbar: {
116159
items: [ 'foo', 'bar' ],
117160
viewportTopOffset: 100
@@ -129,7 +172,7 @@ describe( 'DecoupledEditorUI', () => {
129172
} );
130173

131174
it( 'initializes keyboard navigation between view#toolbar and view#editable', () => {
132-
return VirtualDecoupledTestEditor.create()
175+
return VirtualDecoupledTestEditor.create( '' )
133176
.then( editor => {
134177
const ui = editor.ui;
135178
const view = ui.view;
@@ -152,6 +195,47 @@ describe( 'DecoupledEditorUI', () => {
152195
} );
153196
} );
154197

198+
describe( 'destroy()', () => {
199+
it( 'detaches the DOM root then destroys the UI view', () => {
200+
return VirtualDecoupledTestEditor.create( '' )
201+
.then( newEditor => {
202+
const destroySpy = sinon.spy( newEditor.ui.view, 'destroy' );
203+
const detachSpy = sinon.spy( newEditor.editing.view, 'detachDomRoot' );
204+
205+
return newEditor.destroy()
206+
.then( () => {
207+
sinon.assert.callOrder( detachSpy, destroySpy );
208+
} );
209+
} );
210+
} );
211+
212+
it( 'restores the editor element back to its original state', () => {
213+
const domElement = document.createElement( 'div' );
214+
215+
domElement.setAttribute( 'foo', 'bar' );
216+
domElement.setAttribute( 'data-baz', 'qux' );
217+
domElement.classList.add( 'foo-class' );
218+
219+
return VirtualDecoupledTestEditor.create( domElement )
220+
.then( newEditor => {
221+
return newEditor.destroy()
222+
.then( () => {
223+
const attributes = {};
224+
225+
for ( const attribute of domElement.attributes ) {
226+
attributes[ attribute.name ] = attribute.value;
227+
}
228+
229+
expect( attributes ).to.deep.equal( {
230+
foo: 'bar',
231+
'data-baz': 'qux',
232+
class: 'foo-class'
233+
} );
234+
} );
235+
} );
236+
} );
237+
} );
238+
155239
describe( 'element()', () => {
156240
it( 'returns correct element instance', () => {
157241
expect( ui.element ).to.equal( viewElement );
@@ -185,10 +269,14 @@ function viewCreator( name ) {
185269
}
186270

187271
class VirtualDecoupledTestEditor extends VirtualTestEditor {
188-
constructor( config ) {
272+
constructor( sourceElementOrData, config ) {
189273
super( config );
190274

191-
const view = new DecoupledEditorUIView( this.locale );
275+
if ( isElement( sourceElementOrData ) ) {
276+
this.sourceElement = sourceElementOrData;
277+
}
278+
279+
const view = new DecoupledEditorUIView( this.locale, this.editing.view );
192280
this.ui = new DecoupledEditorUI( this, view );
193281

194282
this.ui.componentFactory.add( 'foo', viewCreator( 'foo' ) );
@@ -201,14 +289,20 @@ class VirtualDecoupledTestEditor extends VirtualTestEditor {
201289
return super.destroy();
202290
}
203291

204-
static create( config ) {
292+
static create( sourceElementOrData, config ) {
205293
return new Promise( resolve => {
206-
const editor = new this( config );
294+
const editor = new this( sourceElementOrData, config );
207295

208296
resolve(
209297
editor.initPlugins()
210298
.then( () => {
211299
editor.ui.init();
300+
301+
const initialData = isElement( sourceElementOrData ) ?
302+
sourceElementOrData.innerHTML :
303+
sourceElementOrData;
304+
305+
editor.data.init( initialData );
212306
editor.fire( 'ready' );
213307
} )
214308
.then( () => editor )

0 commit comments

Comments
 (0)