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

Commit 24016bd

Browse files
author
Piotr Jasiun
authored
Merge pull request #45 from ckeditor/t/ckeditor5/479
Feature: Enabled the inline editor placeholder (see ckeditor/ckeditor5#479). BREAKING CHANGE: The second argument of `InlineEditorUIView.constructor()` is an editing view instance now.
2 parents dca4fe2 + 7e80670 commit 24016bd

File tree

9 files changed

+269
-49
lines changed

9 files changed

+269
-49
lines changed

src/inlineeditor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ export default class InlineEditor extends Editor {
7070
this.sourceElement = sourceElementOrData;
7171
}
7272

73-
this.ui = new InlineEditorUI( this, new InlineEditorUIView( this.locale, this.sourceElement ) );
73+
const view = new InlineEditorUIView( this.locale, this.editing.view, this.sourceElement );
74+
this.ui = new InlineEditorUI( this, view );
7475

7576
attachToForm( this );
7677
}

src/inlineeditorui.js

Lines changed: 87 additions & 25 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 inline editor UI class.
@@ -56,9 +57,72 @@ export default class InlineEditorUI extends EditorUI {
5657
init() {
5758
const editor = this.editor;
5859
const view = this.view;
60+
const editingView = editor.editing.view;
61+
const editable = view.editable;
62+
const editingRoot = editingView.document.getRoot();
63+
64+
// The editable UI and editing root should share the same name. Then name is used
65+
// to recognize the particular editable, for instance in ARIA attributes.
66+
editable.name = editingRoot.rootName;
5967

6068
view.render();
6169

70+
// The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
71+
// But it can be available earlier if a DOM element has been passed to InlineEditor.create().
72+
const editableElement = editable.element;
73+
74+
// Register the editable UI view in the editor. A single editor instance can aggregate multiple
75+
// editable areas (roots) but the inline editor has only one.
76+
this._editableElements.set( editable.name, editableElement );
77+
78+
// Let the global focus tracker know that the editable UI element is focusable and
79+
// belongs to the editor. From now on, the focus tracker will sustain the editor focus
80+
// as long as the editable is focused (e.g. the user is typing).
81+
this.focusTracker.add( editableElement );
82+
83+
// Let the editable UI element respond to the changes in the global editor focus
84+
// tracker. It has been added to the same tracker a few lines above but, in reality, there are
85+
// many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
86+
// as they have focus, the editable should act like it is focused too (although technically
87+
// it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
88+
// Doing otherwise will result in editable focus styles disappearing, once e.g. the
89+
// toolbar gets focused.
90+
editable.bind( 'isFocused' ).to( this.focusTracker );
91+
92+
// Bind the editable UI element to the editing view, making it an end– and entry–point
93+
// of the editor's engine. This is where the engine meets the UI.
94+
editingView.attachDomRoot( editableElement );
95+
96+
this._initPlaceholder();
97+
this._initToolbar();
98+
this.fire( 'ready' );
99+
}
100+
101+
/**
102+
* @inheritDoc
103+
*/
104+
destroy() {
105+
const view = this.view;
106+
const editingView = this.editor.editing.view;
107+
108+
editingView.detachDomRoot( view.editable.name );
109+
view.destroy();
110+
111+
super.destroy();
112+
}
113+
114+
/**
115+
* Initializes the inline editor toolbar and its panel.
116+
*
117+
* @private
118+
*/
119+
_initToolbar() {
120+
const editor = this.editor;
121+
const view = this.view;
122+
const editableElement = view.editable.element;
123+
const editingView = editor.editing.view;
124+
const toolbar = view.toolbar;
125+
62126
// Set–up the view#panel.
63127
view.panel.bind( 'isVisible' ).to( this.focusTracker, 'isFocused' );
64128

@@ -72,44 +136,42 @@ export default class InlineEditorUI extends EditorUI {
72136
// showing up when there's no focus in the UI.
73137
if ( view.panel.isVisible ) {
74138
view.panel.pin( {
75-
target: view.editable.element,
139+
target: editableElement,
76140
positions: view.panelPositions
77141
} );
78142
}
79143
} );
80144

81-
// Setup the editable.
82-
const editingRoot = editor.editing.view.document.getRoot();
83-
view.editable.bind( 'isReadOnly' ).to( editingRoot );
84-
85-
// Bind to focusTracker instead of editor.editing.view because otherwise
86-
// focused editable styles disappear when view#toolbar is focused.
87-
view.editable.bind( 'isFocused' ).to( this.focusTracker );
88-
editor.editing.view.attachDomRoot( view.editable.element );
89-
view.editable.name = editingRoot.rootName;
90-
91-
this._editableElements.set( view.editable.name, view.editable.element );
92-
93-
this.focusTracker.add( view.editable.element );
94-
95-
view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
145+
toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
96146

97147
enableToolbarKeyboardFocus( {
98-
origin: editor.editing.view,
148+
origin: editingView,
99149
originFocusTracker: this.focusTracker,
100150
originKeystrokeHandler: editor.keystrokes,
101-
toolbar: view.toolbar
151+
toolbar
102152
} );
103-
104-
this.fire( 'ready' );
105153
}
106154

107155
/**
108-
* @inheritDoc
156+
* Enable the placeholder text on the editing root, if any was configured.
157+
*
158+
* @private
109159
*/
110-
destroy() {
111-
this.view.destroy();
112-
113-
super.destroy();
160+
_initPlaceholder() {
161+
const editor = this.editor;
162+
const editingView = editor.editing.view;
163+
const editingRoot = editingView.document.getRoot();
164+
165+
const placeholderText = editor.config.get( 'placeholder' ) ||
166+
editor.sourceElement && editor.sourceElement.getAttribute( 'placeholder' );
167+
168+
if ( placeholderText ) {
169+
enablePlaceholder( {
170+
view: editingView,
171+
element: editingRoot,
172+
text: placeholderText,
173+
isDirectHost: false
174+
} );
175+
}
114176
}
115177
}

src/inlineeditoruiview.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ export default class InlineEditorUIView extends EditorUIView {
2222
* Creates an instance of the inline editor UI view.
2323
*
2424
* @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
25+
* @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
2526
* @param {HTMLElement} [editableElement] The editable element. If not specified, it will be automatically created by
2627
* {@link module:ui/editableui/editableuiview~EditableUIView}. Otherwise, the given element will be used.
2728
*/
28-
constructor( locale, editableElement ) {
29+
constructor( locale, editingView, editableElement ) {
2930
super( locale );
3031

3132
/**
@@ -129,7 +130,7 @@ export default class InlineEditorUIView extends EditorUIView {
129130
* @readonly
130131
* @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
131132
*/
132-
this.editable = new InlineEditableUIView( locale, editableElement );
133+
this.editable = new InlineEditableUIView( locale, editingView, editableElement );
133134
}
134135

135136
/**

tests/inlineeditorui.js

Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
1010
import InlineEditorUI from '../src/inlineeditorui';
1111
import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
1212
import InlineEditorUIView from '../src/inlineeditoruiview';
13+
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
1314
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
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( 'InlineEditorUI', () => {
2022
let editor, view, ui, viewElement;
@@ -23,7 +25,7 @@ describe( 'InlineEditorUI', () => {
2325

2426
beforeEach( () => {
2527
return VirtualInlineTestEditor
26-
.create( {
28+
.create( 'foo', {
2729
toolbar: [ 'foo', 'bar' ],
2830
} )
2931
.then( newEditor => {
@@ -65,7 +67,7 @@ describe( 'InlineEditorUI', () => {
6567

6668
it( 'sets view#viewportTopOffset, if specified', () => {
6769
return VirtualInlineTestEditor
68-
.create( {
70+
.create( 'foo', {
6971
toolbar: {
7072
viewportTopOffset: 100
7173
}
@@ -128,23 +130,66 @@ describe( 'InlineEditorUI', () => {
128130
{ isFocused: true }
129131
);
130132
} );
133+
} );
131134

132-
it( 'binds view.editable#isReadOnly', () => {
133-
utils.assertBinding(
134-
view.editable,
135-
{ isReadOnly: false },
136-
[
137-
[ editable, { isReadOnly: true } ]
138-
],
139-
{ isReadOnly: true }
140-
);
135+
describe( 'placeholder', () => {
136+
it( 'sets placeholder from editor.config.placeholder', () => {
137+
return VirtualInlineTestEditor
138+
.create( 'foo', {
139+
extraPlugins: [ Paragraph ],
140+
placeholder: 'placeholder-text',
141+
} )
142+
.then( newEditor => {
143+
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
144+
145+
expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
146+
147+
return newEditor.destroy();
148+
} );
149+
} );
150+
151+
it( 'sets placeholder from "placeholder" attribute of a passed element', () => {
152+
const element = document.createElement( 'div' );
153+
154+
element.setAttribute( 'placeholder', 'placeholder-text' );
155+
156+
return VirtualInlineTestEditor
157+
.create( element, {
158+
extraPlugins: [ Paragraph ]
159+
} )
160+
.then( newEditor => {
161+
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
162+
163+
expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
164+
165+
return newEditor.destroy();
166+
} );
167+
} );
168+
169+
it( 'uses editor.config.placeholder rather than "placeholder" attribute of a passed element', () => {
170+
const element = document.createElement( 'div' );
171+
172+
element.setAttribute( 'placeholder', 'placeholder-text' );
173+
174+
return VirtualInlineTestEditor
175+
.create( element, {
176+
placeholder: 'config takes precedence',
177+
extraPlugins: [ Paragraph ]
178+
} )
179+
.then( newEditor => {
180+
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
181+
182+
expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'config takes precedence' );
183+
184+
return newEditor.destroy();
185+
} );
141186
} );
142187
} );
143188

144189
describe( 'view.toolbar#items', () => {
145190
it( 'are filled with the config.toolbar (specified as an Array)', () => {
146191
return VirtualInlineTestEditor
147-
.create( {
192+
.create( '', {
148193
toolbar: [ 'foo', 'bar' ]
149194
} )
150195
.then( editor => {
@@ -159,7 +204,7 @@ describe( 'InlineEditorUI', () => {
159204

160205
it( 'are filled with the config.toolbar (specified as an Object)', () => {
161206
return VirtualInlineTestEditor
162-
.create( {
207+
.create( '', {
163208
toolbar: {
164209
items: [ 'foo', 'bar' ],
165210
viewportTopOffset: 100
@@ -177,7 +222,7 @@ describe( 'InlineEditorUI', () => {
177222
} );
178223

179224
it( 'initializes keyboard navigation between view#toolbar and view#editable', () => {
180-
return VirtualInlineTestEditor.create()
225+
return VirtualInlineTestEditor.create( '' )
181226
.then( editor => {
182227
const ui = editor.ui;
183228
const view = ui.view;
@@ -200,6 +245,47 @@ describe( 'InlineEditorUI', () => {
200245
} );
201246
} );
202247

248+
describe( 'destroy()', () => {
249+
it( 'detaches the DOM root then destroys the UI view', () => {
250+
return VirtualInlineTestEditor.create( '' )
251+
.then( newEditor => {
252+
const destroySpy = sinon.spy( newEditor.ui.view, 'destroy' );
253+
const detachSpy = sinon.spy( newEditor.editing.view, 'detachDomRoot' );
254+
255+
return newEditor.destroy()
256+
.then( () => {
257+
sinon.assert.callOrder( detachSpy, destroySpy );
258+
} );
259+
} );
260+
} );
261+
262+
it( 'restores the editor element back to its original state', () => {
263+
const domElement = document.createElement( 'div' );
264+
265+
domElement.setAttribute( 'foo', 'bar' );
266+
domElement.setAttribute( 'data-baz', 'qux' );
267+
domElement.classList.add( 'foo-class' );
268+
269+
return VirtualInlineTestEditor.create( domElement )
270+
.then( newEditor => {
271+
return newEditor.destroy()
272+
.then( () => {
273+
const attributes = {};
274+
275+
for ( const attribute of domElement.attributes ) {
276+
attributes[ attribute.name ] = attribute.value;
277+
}
278+
279+
expect( attributes ).to.deep.equal( {
280+
foo: 'bar',
281+
'data-baz': 'qux',
282+
class: 'foo-class'
283+
} );
284+
} );
285+
} );
286+
} );
287+
} );
288+
203289
describe( 'element()', () => {
204290
it( 'returns correct element instance', () => {
205291
expect( ui.element ).to.equal( viewElement );
@@ -233,10 +319,14 @@ function viewCreator( name ) {
233319
}
234320

235321
class VirtualInlineTestEditor extends VirtualTestEditor {
236-
constructor( config ) {
322+
constructor( sourceElementOrData, config ) {
237323
super( config );
238324

239-
const view = new InlineEditorUIView( this.locale );
325+
if ( isElement( sourceElementOrData ) ) {
326+
this.sourceElement = sourceElementOrData;
327+
}
328+
329+
const view = new InlineEditorUIView( this.locale, this.editing.view );
240330
this.ui = new InlineEditorUI( this, view );
241331

242332
this.ui.componentFactory.add( 'foo', viewCreator( 'foo' ) );
@@ -249,14 +339,20 @@ class VirtualInlineTestEditor extends VirtualTestEditor {
249339
return super.destroy();
250340
}
251341

252-
static create( config ) {
342+
static create( sourceElementOrData, config ) {
253343
return new Promise( resolve => {
254-
const editor = new this( config );
344+
const editor = new this( sourceElementOrData, config );
255345

256346
resolve(
257347
editor.initPlugins()
258348
.then( () => {
259349
editor.ui.init();
350+
351+
const initialData = isElement( sourceElementOrData ) ?
352+
sourceElementOrData.innerHTML :
353+
sourceElementOrData;
354+
355+
editor.data.init( initialData );
260356
editor.fire( 'ready' );
261357
} )
262358
.then( () => editor )

0 commit comments

Comments
 (0)