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

Commit

Permalink
Merge pull request #26 from ckeditor/t/ckeditor5/479
Browse files Browse the repository at this point in the history
Feature: Enabled the balloon editor placeholder (see ckeditor/ckeditor5#479).

BREAKING CHANGE: The second argument of `BalloonEditorUIView.constructor()` is an editing view instance now.
  • Loading branch information
Piotr Jasiun authored Feb 7, 2019
2 parents 77b2de4 + 767c1f9 commit 7f39e5e
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 32 deletions.
3 changes: 2 additions & 1 deletion src/ballooneditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export default class BalloonEditor extends Editor {

this.model.document.createRoot();

this.ui = new BalloonEditorUI( this, new BalloonEditorUIView( this.locale, this.sourceElement ) );
const view = new BalloonEditorUIView( this.locale, this.editing.view, this.sourceElement );
this.ui = new BalloonEditorUI( this, view );

attachToForm( this );
}
Expand Down
72 changes: 60 additions & 12 deletions src/ballooneditorui.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';

/**
* The balloon editor UI class.
Expand Down Expand Up @@ -48,25 +49,44 @@ export default class BalloonEditorUI extends EditorUI {
const editor = this.editor;
const view = this.view;
const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
const editingView = editor.editing.view;
const editable = view.editable;
const editingRoot = editingView.document.getRoot();

// The editable UI and editing root should share the same name. Then name is used
// to recognize the particular editable, for instance in ARIA attributes.
editable.name = editingRoot.rootName;

view.render();

// Setup the editable.
const editingRoot = editor.editing.view.document.getRoot();
view.editable.bind( 'isReadOnly' ).to( editingRoot );
// The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
// But it can be available earlier if a DOM element has been passed to BalloonEditor.create().
const editableElement = editable.element;

// Register the editable UI view in the editor. A single editor instance can aggregate multiple
// editable areas (roots) but the balloon editor has only one.
this._editableElements.set( editable.name, editableElement );

// Bind to focusTracker instead of editor.editing.view because otherwise
// focused editable styles disappear when view#toolbar is focused.
view.editable.bind( 'isFocused' ).to( this.focusTracker );
editor.editing.view.attachDomRoot( view.editable.element );
view.editable.name = editingRoot.rootName;
// Let the global focus tracker know that the editable UI element is focusable and
// belongs to the editor. From now on, the focus tracker will sustain the editor focus
// as long as the editable is focused (e.g. the user is typing).
this.focusTracker.add( editableElement );

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

this.focusTracker.add( view.editable.element );
// Bind the editable UI element to the editing view, making it an end– and entry–point
// of the editor's engine. This is where the engine meets the UI.
editingView.attachDomRoot( editableElement );

enableToolbarKeyboardFocus( {
origin: editor.editing.view,
origin: editingView,
originFocusTracker: this.focusTracker,
originKeystrokeHandler: editor.keystrokes,
toolbar: balloonToolbar.toolbarView,
Expand All @@ -78,15 +98,43 @@ export default class BalloonEditorUI extends EditorUI {
}
} );

this._initPlaceholder();
this.fire( 'ready' );
}

/**
* @inheritDoc
*/
destroy() {
this.view.destroy();
const view = this.view;
const editingView = this.editor.editing.view;

editingView.detachDomRoot( view.editable.name );
view.destroy();

super.destroy();
}

/**
* Enable the placeholder text on the editing root, if any was configured.
*
* @private
*/
_initPlaceholder() {
const editor = this.editor;
const editingView = editor.editing.view;
const editingRoot = editingView.document.getRoot();

const placeholderText = editor.config.get( 'placeholder' ) ||
editor.sourceElement && editor.sourceElement.getAttribute( 'placeholder' );

if ( placeholderText ) {
enablePlaceholder( {
view: editingView,
element: editingRoot,
text: placeholderText,
isDirectHost: false
} );
}
}
}
5 changes: 3 additions & 2 deletions src/ballooneditoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ export default class BalloonEditorUIView extends EditorUIView {
* Creates an instance of the balloon editor UI view.
*
* @param {module:utils/locale~Locale} locale The {@link module:core/editor/editor~Editor#locale} instance.
* @param {module:engine/view/view~View} editingView The editing view instance this view is related to.
* @param {HTMLElement} [editableElement] The editable element. If not specified, it will be automatically created by
* {@link module:ui/editableui/editableuiview~EditableUIView}. Otherwise, the given element will be used.
*/
constructor( locale, editableElement ) {
constructor( locale, editingView, editableElement ) {
super( locale );

/**
Expand All @@ -32,7 +33,7 @@ export default class BalloonEditorUIView extends EditorUIView {
* @readonly
* @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
*/
this.editable = new InlineEditableUIView( locale, editableElement );
this.editable = new InlineEditableUIView( locale, editingView, editableElement );
}

/**
Expand Down
132 changes: 117 additions & 15 deletions tests/ballooneditorui.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
* For licensing, see LICENSE.md.
*/

/* globals Event */
/* globals document, Event */

import BalloonEditorUI from '../src/ballooneditorui';
import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
import BalloonEditorUIView from '../src/ballooneditoruiview';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloontoolbar';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import { isElement } from 'lodash-es';

import utils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
Expand All @@ -22,7 +24,7 @@ describe( 'BalloonEditorUI', () => {

beforeEach( () => {
return VirtualBalloonTestEditor
.create( {
.create( 'foo', {
plugins: [ BalloonToolbar ]
} )
.then( newEditor => {
Expand Down Expand Up @@ -111,20 +113,110 @@ describe( 'BalloonEditorUI', () => {
{ isFocused: true }
);
} );
} );

it( 'binds view.editable#isReadOnly', () => {
utils.assertBinding(
view.editable,
{ isReadOnly: false },
[
[ editable, { isReadOnly: true } ]
],
{ isReadOnly: true }
);
describe( 'placeholder', () => {
it( 'sets placeholder from editor.config.placeholder', () => {
return VirtualBalloonTestEditor
.create( 'foo', {
extraPlugins: [ BalloonToolbar, Paragraph ],
placeholder: 'placeholder-text',
} )
.then( newEditor => {
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );

expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );

return newEditor.destroy();
} );
} );

it( 'sets placeholder from "placeholder" attribute of a passed element', () => {
const element = document.createElement( 'div' );

element.setAttribute( 'placeholder', 'placeholder-text' );

return VirtualBalloonTestEditor
.create( element, {
extraPlugins: [ BalloonToolbar, Paragraph ]
} )
.then( newEditor => {
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );

expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );

return newEditor.destroy();
} );
} );

it( 'uses editor.config.placeholder rather than "placeholder" attribute of a passed element', () => {
const element = document.createElement( 'div' );

element.setAttribute( 'placeholder', 'placeholder-text' );

return VirtualBalloonTestEditor
.create( element, {
placeholder: 'config takes precedence',
extraPlugins: [ BalloonToolbar, Paragraph ]
} )
.then( newEditor => {
const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );

expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'config takes precedence' );

return newEditor.destroy();
} );
} );
} );
} );

describe( 'destroy()', () => {
it( 'detaches the DOM root then destroys the UI view', () => {
return VirtualBalloonTestEditor
.create( '', {
plugins: [ BalloonToolbar ]
} )
.then( newEditor => {
const destroySpy = sinon.spy( newEditor.ui.view, 'destroy' );
const detachSpy = sinon.spy( newEditor.editing.view, 'detachDomRoot' );

return newEditor.destroy()
.then( () => {
sinon.assert.callOrder( detachSpy, destroySpy );
} );
} );
} );

it( 'restores the editor element back to its original state', () => {
const domElement = document.createElement( 'div' );

domElement.setAttribute( 'foo', 'bar' );
domElement.setAttribute( 'data-baz', 'qux' );
domElement.classList.add( 'foo-class' );

return VirtualBalloonTestEditor
.create( domElement, {
plugins: [ BalloonToolbar ]
} )
.then( newEditor => {
return newEditor.destroy()
.then( () => {
const attributes = {};

for ( const attribute of domElement.attributes ) {
attributes[ attribute.name ] = attribute.value;
}

expect( attributes ).to.deep.equal( {
foo: 'bar',
'data-baz': 'qux',
class: 'foo-class'
} );
} );
} );
} );
} );

describe( 'element()', () => {
it( 'returns correct element instance', () => {
expect( ui.element ).to.equal( viewElement );
Expand All @@ -147,10 +239,14 @@ describe( 'BalloonEditorUI', () => {
} );

class VirtualBalloonTestEditor extends VirtualTestEditor {
constructor( config ) {
constructor( sourceElementOrData, config ) {
super( config );

const view = new BalloonEditorUIView( this.locale );
if ( isElement( sourceElementOrData ) ) {
this.sourceElement = sourceElementOrData;
}

const view = new BalloonEditorUIView( this.locale, this.editing.view );
this.ui = new BalloonEditorUI( this, view );
}

Expand All @@ -160,14 +256,20 @@ class VirtualBalloonTestEditor extends VirtualTestEditor {
return super.destroy();
}

static create( config ) {
static create( sourceElementOrData, config ) {
return new Promise( resolve => {
const editor = new this( config );
const editor = new this( sourceElementOrData, config );

resolve(
editor.initPlugins()
.then( () => {
editor.ui.init();

const initialData = isElement( sourceElementOrData ) ?
sourceElementOrData.innerHTML :
sourceElementOrData;

editor.data.init( initialData );
editor.fire( 'ready' );
} )
.then( () => editor )
Expand Down
9 changes: 7 additions & 2 deletions tests/ballooneditoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@
* For licensing, see LICENSE.md.
*/

import EditingView from '@ckeditor/ckeditor5-engine/src/view/view';
import BalloonEditorUIView from '../src/ballooneditoruiview';
import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
import Locale from '@ckeditor/ckeditor5-utils/src/locale';
import createRoot from '@ckeditor/ckeditor5-engine/tests/view/_utils/createroot.js';

import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';

describe( 'BalloonEditorUIView', () => {
let locale, view;
let locale, view, editingView, editingViewRoot;

testUtils.createSinonSandbox();

beforeEach( () => {
locale = new Locale( 'en' );
view = new BalloonEditorUIView( locale );
editingView = new EditingView();
editingViewRoot = createRoot( editingView.document );
view = new BalloonEditorUIView( locale, editingView );
view.editable.name = editingViewRoot.rootName;
} );

describe( 'constructor()', () => {
Expand Down
5 changes: 5 additions & 0 deletions tests/manual/placeholder.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div id="editor-1" placeholder="Placeholder from the attribute">
<p>Remove this text to see the placeholder.</p>
</div>
<br />
<div id="editor-2" placeholder="Placeholder from the attribute"></div>
37 changes: 37 additions & 0 deletions tests/manual/placeholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals console:false, document, window */

import BalloonEditor from '../../src/ballooneditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';

window.editors = {};

function initEditor( element, placeholder ) {
BalloonEditor
.create( element, {
plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ],
toolbar: [ 'heading', '|', 'bold', 'italic', 'undo', 'redo' ],
placeholder
} )
.then( newEditor => {
console.log( 'Editor was initialized', newEditor );

window.editors[ element.id ] = newEditor;
} )
.catch( err => {
console.error( err.stack );
} );
}

initEditor( document.querySelector( '#editor-1' ) );
initEditor( document.querySelector( '#editor-2' ), 'The placeholder from editor.config.placeholder' );
Loading

0 comments on commit 7f39e5e

Please sign in to comment.