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

Commit

Permalink
Merge 2624c8e into 2b861b9
Browse files Browse the repository at this point in the history
  • Loading branch information
oleq committed Jan 28, 2019
2 parents 2b861b9 + 2624c8e commit 7bd6349
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 42 deletions.
3 changes: 2 additions & 1 deletion src/decouplededitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ export default class DecoupledEditor extends Editor {

this.model.document.createRoot();

this.ui = new DecoupledEditorUI( this, new DecoupledEditorUIView( this.locale, this.sourceElement ) );
const view = new DecoupledEditorUIView( this.locale, this.editing.view, this.sourceElement );
this.ui = new DecoupledEditorUI( this, view );
}

/**
Expand Down
95 changes: 79 additions & 16 deletions src/decouplededitorui.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus';
import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalizetoolbarconfig';
import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder';

/**
* The decoupled editor UI class.
Expand Down Expand Up @@ -49,38 +50,100 @@ export default class DecoupledEditorUI extends EditorUI {
init() {
const editor = this.editor;
const view = this.view;
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.
view.editable.name = editingRoot.rootName;

view.render();

// Set up the editable.
const editingRoot = editor.editing.view.document.getRoot();
view.editable.bind( 'isReadOnly' ).to( editingRoot );
view.editable.bind( 'isFocused' ).to( editor.editing.view.document );
editor.editing.view.attachDomRoot( view.editable.element );
view.editable.name = editingRoot.rootName;
// 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 DecoupledEditor.create().
const editableElement = editable.element;

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

this.focusTracker.add( view.editable.element );
// 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.view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );
// 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.
view.editable.bind( 'isFocused' ).to( this.focusTracker );

enableToolbarKeyboardFocus( {
origin: editor.editing.view,
originFocusTracker: this.focusTracker,
originKeystrokeHandler: editor.keystrokes,
toolbar: this.view.toolbar
} );
// 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 );

this._initPlaceholder();
this._initToolbar();
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();
}

/**
* Initializes the inline editor toolbar and its panel.
*
* @private
*/
_initToolbar() {
const editor = this.editor;
const view = this.view;
const toolbar = view.toolbar;

toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory );

enableToolbarKeyboardFocus( {
origin: editor.editing.view,
originFocusTracker: this.focusTracker,
originKeystrokeHandler: editor.keystrokes,
toolbar
} );
}

/**
* 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/decouplededitoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ export default class DecoupledEditorUIView extends EditorUIView {
* Creates an instance of the decoupled 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 @@ -48,7 +49,7 @@ export default class DecoupledEditorUIView extends EditorUIView {
* @readonly
* @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
*/
this.editable = new InlineEditableUIView( locale, editableElement );
this.editable = new InlineEditableUIView( locale, editingView, editableElement );

// This toolbar may be placed anywhere in the page so things like font size need to be reset in it.
// Also because of the above, make sure the toolbar supports rounded corners.
Expand Down
93 changes: 73 additions & 20 deletions tests/decouplededitorui.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import DecoupledEditorUI from '../src/decouplededitorui';
import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import DecoupledEditorUIView from '../src/decouplededitoruiview';

import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import utils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
import { isElement } from 'lodash-es';

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

beforeEach( () => {
return VirtualDecoupledTestEditor
.create( {
.create( '', {
toolbar: [ 'foo', 'bar' ],
} )
.then( newEditor => {
Expand Down Expand Up @@ -69,34 +71,81 @@ describe( 'DecoupledEditorUI', () => {
view.editable,
{ isFocused: false },
[
[ editor.editing.view.document, { isFocused: true } ]
[ ui.focusTracker, { isFocused: true } ]
],
{ isFocused: true }
);
} );

it( 'binds view.editable#isReadOnly', () => {
const editable = editor.editing.view.document.getRoot();
it( 'attaches editable UI as view\'s DOM root', () => {
expect( editor.editing.view.getDomRoot() ).to.equal( view.editable.element );
} );
} );

utils.assertBinding(
view.editable,
{ isReadOnly: false },
[
[ editable, { isReadOnly: true } ]
],
{ isReadOnly: true }
);
describe( 'placeholder', () => {
it( 'sets placeholder from editor.config.placeholder', () => {
return VirtualDecoupledTestEditor
.create( 'foo', {
extraPlugins: [ Paragraph ],
placeholder: 'placeholder-text',
} )
.then( newEditor => {
editor = newEditor;

const firstChild = editor.editing.view.document.getRoot().getChild( 0 );

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

return editor.destroy();
} );
} );

it( 'attaches editable UI as view\'s DOM root', () => {
expect( editor.editing.view.getDomRoot() ).to.equal( view.editable.element );
it( 'sets placeholder from "placeholder" attribute of a passed element', () => {
const element = document.createElement( 'div' );

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

return VirtualDecoupledTestEditor
.create( element, {
extraPlugins: [ Paragraph ]
} )
.then( newEditor => {
editor = newEditor;

const firstChild = editor.editing.view.document.getRoot().getChild( 0 );

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

return editor.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 VirtualDecoupledTestEditor
.create( element, {
placeholder: 'config takes precedence',
extraPlugins: [ Paragraph ]
} )
.then( newEditor => {
editor = newEditor;

const firstChild = editor.editing.view.document.getRoot().getChild( 0 );

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

return editor.destroy();
} );
} );
} );

describe( 'view.toolbar#items', () => {
it( 'are filled with the config.toolbar (specified as an Array)', () => {
return VirtualDecoupledTestEditor
.create( {
.create( '', {
toolbar: [ 'foo', 'bar' ]
} )
.then( editor => {
Expand All @@ -111,7 +160,7 @@ describe( 'DecoupledEditorUI', () => {

it( 'are filled with the config.toolbar (specified as an Object)', () => {
return VirtualDecoupledTestEditor
.create( {
.create( '', {
toolbar: {
items: [ 'foo', 'bar' ],
viewportTopOffset: 100
Expand Down Expand Up @@ -185,10 +234,14 @@ function viewCreator( name ) {
}

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

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

const view = new DecoupledEditorUIView( this.locale, this.editing.view );
this.ui = new DecoupledEditorUI( this, view );

this.ui.componentFactory.add( 'foo', viewCreator( 'foo' ) );
Expand All @@ -201,9 +254,9 @@ class VirtualDecoupledTestEditor 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()
Expand Down
18 changes: 15 additions & 3 deletions tests/decouplededitoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@
/* globals document */

import DecoupledEditorUIView from '../src/decouplededitoruiview';
import EditingView from '@ckeditor/ckeditor5-engine/src/view/view';
import ViewRootEditableElement from '@ckeditor/ckeditor5-engine/src/view/rooteditableelement';
import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
import Locale from '@ckeditor/ckeditor5-utils/src/locale';

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

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

testUtils.createSinonSandbox();

beforeEach( () => {
locale = new Locale( 'en' );
view = new DecoupledEditorUIView( locale );
setUpEditingView();
view = new DecoupledEditorUIView( locale, editingView );
view.editable.name = editingViewRoot.rootName;
} );

describe( 'constructor()', () => {
Expand Down Expand Up @@ -57,7 +61,8 @@ describe( 'DecoupledEditorUIView', () => {

it( 'can be created out of an existing DOM element', () => {
const editableElement = document.createElement( 'div' );
const testView = new DecoupledEditorUIView( locale, editableElement );
const testView = new DecoupledEditorUIView( locale, editingView, editableElement );
testView.editable.name = editingViewRoot.rootName;

testView.render();

Expand Down Expand Up @@ -125,4 +130,11 @@ describe( 'DecoupledEditorUIView', () => {
view.editable.element.remove();
} );
} );

function setUpEditingView() {
editingView = new EditingView();
editingViewRoot = new ViewRootEditableElement( 'div' );
editingViewRoot._document = editingView.document;
editingView.document.roots.add( editingViewRoot );
}
} );
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>

0 comments on commit 7bd6349

Please sign in to comment.