From 23a8f8be10992030fd508cfa135d19cba8f07e52 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 17 Jan 2019 13:29:44 +0100 Subject: [PATCH 01/17] Draft of the placeholder feature. --- src/decouplededitor.js | 3 ++- src/decouplededitorui.js | 18 ++++++++++++++---- src/decouplededitoruiview.js | 4 ++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/decouplededitor.js b/src/decouplededitor.js index b59218f..6f80333 100644 --- a/src/decouplededitor.js +++ b/src/decouplededitor.js @@ -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.sourceElement, this.editing.view ); + this.ui = new DecoupledEditorUI( this, view ); } /** diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index b23df4a..09b25af 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -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 { attachPlaceholder, getPlaceholderElement } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; /** * The decoupled editor UI class. @@ -38,16 +39,25 @@ export default class DecoupledEditorUI extends EditorUI { init() { const editor = this.editor; const view = this.view; + const editingView = editor.editing.view; 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.editableElement ); + + const editingRoot = editor.editing.view.document.getRoot(); + view.editable.name = editingRoot.rootName; + editor.on( 'dataReady', () => { + view.editable.enableDomRootActions(); + + attachPlaceholder( editingView, getPlaceholderElement( editingRoot ), 'Type some text...' ); + } ); + + // Set up the editable. + view.editable.bind( 'isFocused' ).to( editor.editing.view.document ); + this.focusTracker.add( this.view.editableElement ); this.view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory ); diff --git a/src/decouplededitoruiview.js b/src/decouplededitoruiview.js index 03e7283..b438fcd 100644 --- a/src/decouplededitoruiview.js +++ b/src/decouplededitoruiview.js @@ -31,7 +31,7 @@ export default class DecoupledEditorUIView extends EditorUIView { * @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, editableElement, editingView ) { super( locale ); /** @@ -48,7 +48,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. From 2b7135f54202dccd9eb0f29952fe3c4db0e3d459 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 18 Jan 2019 14:28:54 +0100 Subject: [PATCH 02/17] Code refactoring and new documentation in the DecoupledEditorUI class. --- src/decouplededitorui.js | 57 ++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 0db9aca..8ebd94a 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -61,36 +61,71 @@ export default class DecoupledEditorUI extends EditorUI { const editor = this.editor; const view = this.view; const editingView = editor.editing.view; + const editable = view.editable; + const editingRoot = editingView.document.getRoot(); view.render(); - editor.editing.view.attachDomRoot( view.editable.editableElement ); + // 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.editableElement; - const editingRoot = editor.editing.view.document.getRoot(); + // 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.push( view.editable ); + // 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 ); + + // 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 ); + + // 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; + // 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 ); + + // The UI must wait until the data is ready to attach certain actions that operate + // on the editing view–level. They use the view writer to set attributes on the editable + // element and doing so before data is loaded into the model (ready) would destroy the + // original content. editor.on( 'dataReady', () => { view.editable.enableDomRootActions(); - attachPlaceholder( editingView, getPlaceholderElement( editingRoot ), 'Type some text...' ); } ); - // Set up the editable. - view.editable.bind( 'isFocused' ).to( editor.editing.view.document ); + this._initToolbar(); + this.ready(); + } - this._editableElements.push( view.editable ); + /** + * Initializes the inline editor toolbar and its panel. + * + * @private + */ + _initToolbar() { + const editor = this.editor; + const view = this.view; + const toolbar = view.toolbar; - this.focusTracker.add( view.editable.editableElement ); - this.view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory ); + toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory ); enableToolbarKeyboardFocus( { origin: editor.editing.view, originFocusTracker: this.focusTracker, originKeystrokeHandler: editor.keystrokes, - toolbar: this.view.toolbar + toolbar } ); - - this.ready(); } } From a2d12206a7f6e0f23ae405cf508af4bd10d44274 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 18 Jan 2019 14:49:23 +0100 Subject: [PATCH 03/17] Unified the UI destruction process across the editors. --- src/decouplededitorui.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 8ebd94a..6da3883 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -109,6 +109,16 @@ export default class DecoupledEditorUI extends EditorUI { this.ready(); } + /** + * @inheritDoc + */ + destroy() { + this.view.editable.disableDomRootActions(); + this.editor.editing.view.detachDomRoots(); + + super.destroy(); + } + /** * Initializes the inline editor toolbar and its panel. * From fec0cb2dbac59c35fbb7267466c1b676f9f66752 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 18 Jan 2019 16:10:38 +0100 Subject: [PATCH 04/17] Renamed EditableUiView methods related to editing root listeners. --- src/decouplededitorui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 6da3883..6c30fe2 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -101,7 +101,7 @@ export default class DecoupledEditorUI extends EditorUI { // element and doing so before data is loaded into the model (ready) would destroy the // original content. editor.on( 'dataReady', () => { - view.editable.enableDomRootActions(); + view.editable.enableEditingRootListeners(); attachPlaceholder( editingView, getPlaceholderElement( editingRoot ), 'Type some text...' ); } ); @@ -113,7 +113,7 @@ export default class DecoupledEditorUI extends EditorUI { * @inheritDoc */ destroy() { - this.view.editable.disableDomRootActions(); + this.view.editable.disableEditingRootListeners(); this.editor.editing.view.detachDomRoots(); super.destroy(); From 8a7dc6e89114aed4288c547188b494eb315ce2e8 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 18 Jan 2019 16:57:54 +0100 Subject: [PATCH 05/17] Allowed configurable placeholder text. Added placeholder manual test. --- src/decouplededitorui.js | 9 +++++++- tests/manual/placeholder.html | 3 +++ tests/manual/placeholder.js | 39 +++++++++++++++++++++++++++++++++++ tests/manual/placeholder.md | 1 + 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/manual/placeholder.html create mode 100644 tests/manual/placeholder.js create mode 100644 tests/manual/placeholder.md diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 6c30fe2..a1f8fce 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -102,7 +102,14 @@ export default class DecoupledEditorUI extends EditorUI { // original content. editor.on( 'dataReady', () => { view.editable.enableEditingRootListeners(); - attachPlaceholder( editingView, getPlaceholderElement( editingRoot ), 'Type some text...' ); + + const placeholderText = editor.config.get( 'placeholder' ) || editor.sourceElement.getAttribute( 'placeholder' ); + + if ( placeholderText ) { + const placeholderElement = getPlaceholderElement( editingRoot ); + + attachPlaceholder( editingView, placeholderElement, placeholderText ); + } } ); this._initToolbar(); diff --git a/tests/manual/placeholder.html b/tests/manual/placeholder.html new file mode 100644 index 0000000..c4df2aa --- /dev/null +++ b/tests/manual/placeholder.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/tests/manual/placeholder.js b/tests/manual/placeholder.js new file mode 100644 index 0000000..7e160a9 --- /dev/null +++ b/tests/manual/placeholder.js @@ -0,0 +1,39 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console:false, document, window */ + +import DecoupledEditor from '../../src/decouplededitor'; +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 ) { + DecoupledEditor + .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 ); + + element.parentNode.insertBefore( newEditor.ui.view.toolbar.element, element ); + + 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' ); diff --git a/tests/manual/placeholder.md b/tests/manual/placeholder.md new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/tests/manual/placeholder.md @@ -0,0 +1 @@ +TODO \ No newline at end of file From 3cc6ad743f66b8c8feb78208d8587166ddad6068 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 21 Jan 2019 11:00:59 +0100 Subject: [PATCH 06/17] Refactoring: Renamed getPlaceholderElement to getRootPlaceholderElement. --- src/decouplededitorui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index a1f8fce..08e35c1 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -10,7 +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 { attachPlaceholder, getPlaceholderElement } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; +import { attachPlaceholder, getRootPlaceholderElement } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; /** * The decoupled editor UI class. @@ -106,7 +106,7 @@ export default class DecoupledEditorUI extends EditorUI { const placeholderText = editor.config.get( 'placeholder' ) || editor.sourceElement.getAttribute( 'placeholder' ); if ( placeholderText ) { - const placeholderElement = getPlaceholderElement( editingRoot ); + const placeholderElement = getRootPlaceholderElement( editingRoot ); attachPlaceholder( editingView, placeholderElement, placeholderText ); } From 761f9491fe8a656a71566a09271a512432b92df1 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 21 Jan 2019 11:10:40 +0100 Subject: [PATCH 07/17] Renamed the attachPlaceholder() helper to addPlaceholder(). --- src/decouplededitorui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 08e35c1..e7821f3 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -10,7 +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 { attachPlaceholder, getRootPlaceholderElement } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; +import { addPlaceholder, getRootPlaceholderElement } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; /** * The decoupled editor UI class. @@ -108,7 +108,7 @@ export default class DecoupledEditorUI extends EditorUI { if ( placeholderText ) { const placeholderElement = getRootPlaceholderElement( editingRoot ); - attachPlaceholder( editingView, placeholderElement, placeholderText ); + addPlaceholder( editingView, placeholderElement, placeholderText ); } } ); From bc69b1b36982c3d66d77e24b79b94de755017813 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 21 Jan 2019 12:11:26 +0100 Subject: [PATCH 08/17] Renamed detachDomRoots() to detachDomRoot(). --- src/decouplededitorui.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index e7821f3..6aff50f 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -120,8 +120,11 @@ export default class DecoupledEditorUI extends EditorUI { * @inheritDoc */ destroy() { - this.view.editable.disableEditingRootListeners(); - this.editor.editing.view.detachDomRoots(); + const view = this.view; + const editingView = this.editor.editing.view; + + view.editable.disableEditingRootListeners(); + editingView.detachDomRoot( view.editable.name ); super.destroy(); } From 432b29b0217464344f8eaa4ba58bb580a74cfa9c Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 25 Jan 2019 12:48:36 +0100 Subject: [PATCH 09/17] Got rid of *EditableUIView#enableEditingRootListeners(). Moved to render() because view document root rendering is postponed now. --- src/decouplededitorui.js | 47 +++++++++++++++++++---------------- tests/manual/placeholder.html | 4 ++- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 26e54e5..be6d13f 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -64,16 +64,16 @@ export default class DecoupledEditorUI extends EditorUI { 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(); // 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; - // 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; - // 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 ); @@ -96,23 +96,7 @@ export default class DecoupledEditorUI extends EditorUI { // of the editor's engine. This is where the engine meets the UI. editingView.attachDomRoot( editableElement ); - // The UI must wait until the data is ready to attach certain actions that operate - // on the editing view–level. They use the view writer to set attributes on the editable - // element and doing so before data is loaded into the model (ready) would destroy the - // original content. - editor.on( 'dataReady', () => { - view.editable.enableEditingRootListeners(); - - const placeholderText = editor.config.get( 'placeholder' ) || - editor.sourceElement && editor.sourceElement.getAttribute( 'placeholder' ); - - if ( placeholderText ) { - const placeholderElement = getRootPlaceholderElement( editingRoot ); - - addPlaceholder( editingView, placeholderElement, placeholderText ); - } - } ); - + this._initPlaceholder(); this._initToolbar(); this.fire( 'ready' ); } @@ -124,7 +108,6 @@ export default class DecoupledEditorUI extends EditorUI { const view = this._view; const editingView = this.editor.editing.view; - view.editable.disableEditingRootListeners(); editingView.detachDomRoot( view.editable.name ); view.destroy(); @@ -150,4 +133,24 @@ export default class DecoupledEditorUI extends EditorUI { 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 ) { + const placeholderElement = getRootPlaceholderElement( editingRoot ); + + addPlaceholder( editingView, placeholderElement, placeholderText ); + } + } } diff --git a/tests/manual/placeholder.html b/tests/manual/placeholder.html index c4df2aa..7edde17 100644 --- a/tests/manual/placeholder.html +++ b/tests/manual/placeholder.html @@ -1,3 +1,5 @@ -
+
+

Remove this text to see the placeholder.

+

From 1dc9819520e0a09b7919067fd8935c5301b6cded Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 28 Jan 2019 10:10:21 +0100 Subject: [PATCH 10/17] Aligned to the new EditorUI API. --- src/decouplededitorui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 8778067..4313e5e 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -95,7 +95,7 @@ export default class DecoupledEditorUI extends EditorUI { * @inheritDoc */ destroy() { - const view = this._view; + const view = this.view; const editingView = this.editor.editing.view; editingView.detachDomRoot( view.editable.name ); From a9f7c181a60b748462944e071c46abbfa6045992 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 28 Jan 2019 13:54:31 +0100 Subject: [PATCH 11/17] Aligned to the new placeholder helpers API in the engine. --- src/decouplededitorui.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/decouplededitorui.js b/src/decouplededitorui.js index 4313e5e..f51c949 100644 --- a/src/decouplededitorui.js +++ b/src/decouplededitorui.js @@ -10,7 +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 { addPlaceholder, getRootPlaceholderElement } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; +import { enablePlaceholder } from '@ckeditor/ckeditor5-engine/src/view/placeholder'; /** * The decoupled editor UI class. @@ -138,9 +138,12 @@ export default class DecoupledEditorUI extends EditorUI { editor.sourceElement && editor.sourceElement.getAttribute( 'placeholder' ); if ( placeholderText ) { - const placeholderElement = getRootPlaceholderElement( editingRoot ); - - addPlaceholder( editingView, placeholderElement, placeholderText ); + enablePlaceholder( { + view: editingView, + element: editingRoot, + text: placeholderText, + isDirectHost: false + } ); } } } From 2624c8e73fe75863f113e2bed3d4a7fb9aee45d4 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 28 Jan 2019 16:44:53 +0100 Subject: [PATCH 12/17] Tests: Added tests to check the placeholder feature of the UI. Code refactoring. --- src/decouplededitor.js | 2 +- src/decouplededitoruiview.js | 3 +- tests/decouplededitorui.js | 93 ++++++++++++++++++++++++++-------- tests/decouplededitoruiview.js | 18 +++++-- 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/src/decouplededitor.js b/src/decouplededitor.js index 9702cf6..19ffa2b 100644 --- a/src/decouplededitor.js +++ b/src/decouplededitor.js @@ -73,7 +73,7 @@ export default class DecoupledEditor extends Editor { this.model.document.createRoot(); - const view = new DecoupledEditorUIView( this.locale, this.sourceElement, this.editing.view ); + const view = new DecoupledEditorUIView( this.locale, this.editing.view, this.sourceElement ); this.ui = new DecoupledEditorUI( this, view ); } diff --git a/src/decouplededitoruiview.js b/src/decouplededitoruiview.js index 59142ab..352cf33 100644 --- a/src/decouplededitoruiview.js +++ b/src/decouplededitoruiview.js @@ -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, editingView ) { + constructor( locale, editingView, editableElement ) { super( locale ); /** diff --git a/tests/decouplededitorui.js b/tests/decouplededitorui.js index 6132438..1054afc 100644 --- a/tests/decouplededitorui.js +++ b/tests/decouplededitorui.js @@ -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; @@ -23,7 +25,7 @@ describe( 'DecoupledEditorUI', () => { beforeEach( () => { return VirtualDecoupledTestEditor - .create( { + .create( '', { toolbar: [ 'foo', 'bar' ], } ) .then( newEditor => { @@ -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 => { @@ -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 @@ -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' ) ); @@ -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() diff --git a/tests/decouplededitoruiview.js b/tests/decouplededitoruiview.js index c9e46ad..ada29de 100644 --- a/tests/decouplededitoruiview.js +++ b/tests/decouplededitoruiview.js @@ -6,6 +6,8 @@ /* 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'; @@ -13,13 +15,15 @@ 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()', () => { @@ -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(); @@ -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 ); + } } ); From fdc6688e1255739823b88bbf4c2ef07b5e96de8e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 29 Jan 2019 16:36:11 +0100 Subject: [PATCH 13/17] Tests: Added placeholder manual test description. --- tests/manual/placeholder.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/manual/placeholder.md b/tests/manual/placeholder.md index 30404ce..74deb3a 100644 --- a/tests/manual/placeholder.md +++ b/tests/manual/placeholder.md @@ -1 +1,8 @@ -TODO \ No newline at end of file +## Editor placeholder + +1. Make sure the placeholder is visible in the editor without content. +1. Focus the editor — the placeholder should disappear. +1. Blur the editor — the placeholder should re–appear. +1. Focus the editor with the content +1. Remove the content, no placeholder should be visible. +1. Blur the editor, the placeholder should appear. From 02493ef9042c8255058017309af3f99174719737 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 29 Jan 2019 17:12:59 +0100 Subject: [PATCH 14/17] Tests: Fixed failng UI tests. [skip ci] --- tests/decouplededitorui.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/decouplededitorui.js b/tests/decouplededitorui.js index 3285380..50e01e4 100644 --- a/tests/decouplededitorui.js +++ b/tests/decouplededitorui.js @@ -178,7 +178,7 @@ describe( 'DecoupledEditorUI', () => { } ); it( 'initializes keyboard navigation between view#toolbar and view#editable', () => { - return VirtualDecoupledTestEditor.create() + return VirtualDecoupledTestEditor.create( '' ) .then( editor => { const ui = editor.ui; const view = ui.view; @@ -262,6 +262,12 @@ class VirtualDecoupledTestEditor extends VirtualTestEditor { editor.initPlugins() .then( () => { editor.ui.init(); + + const initialData = isElement( sourceElementOrData ) ? + sourceElementOrData.innerHTML : + sourceElementOrData; + + editor.data.init( initialData ); editor.fire( 'ready' ); } ) .then( () => editor ) From 7b1c628478113d865f20dd0d64287376bbbb0244 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 30 Jan 2019 11:34:13 +0100 Subject: [PATCH 15/17] Tests: Fixed broken tests due to double editor destruction. --- tests/decouplededitorui.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/decouplededitorui.js b/tests/decouplededitorui.js index 50e01e4..540737d 100644 --- a/tests/decouplededitorui.js +++ b/tests/decouplededitorui.js @@ -90,13 +90,11 @@ describe( 'DecoupledEditorUI', () => { placeholder: 'placeholder-text', } ) .then( newEditor => { - editor = newEditor; - - const firstChild = editor.editing.view.document.getRoot().getChild( 0 ); + const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 ); expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' ); - return editor.destroy(); + return newEditor.destroy(); } ); } ); @@ -110,13 +108,11 @@ describe( 'DecoupledEditorUI', () => { extraPlugins: [ Paragraph ] } ) .then( newEditor => { - editor = newEditor; - - const firstChild = editor.editing.view.document.getRoot().getChild( 0 ); + const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 ); expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' ); - return editor.destroy(); + return newEditor.destroy(); } ); } ); @@ -131,13 +127,11 @@ describe( 'DecoupledEditorUI', () => { extraPlugins: [ Paragraph ] } ) .then( newEditor => { - editor = newEditor; - - const firstChild = editor.editing.view.document.getRoot().getChild( 0 ); + const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 ); expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'config takes precedence' ); - return editor.destroy(); + return newEditor.destroy(); } ); } ); } ); From 086eab1ce1288d43773d3930c76bcb63935e8574 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 30 Jan 2019 11:44:36 +0100 Subject: [PATCH 16/17] Tests: Used createRoot helper in the UIView tests. --- tests/decouplededitoruiview.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/decouplededitoruiview.js b/tests/decouplededitoruiview.js index ada29de..fd42fe2 100644 --- a/tests/decouplededitoruiview.js +++ b/tests/decouplededitoruiview.js @@ -7,10 +7,10 @@ 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 createRoot from '@ckeditor/ckeditor5-engine/tests/view/_utils/createroot.js'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; @@ -21,7 +21,8 @@ describe( 'DecoupledEditorUIView', () => { beforeEach( () => { locale = new Locale( 'en' ); - setUpEditingView(); + editingView = new EditingView(); + editingViewRoot = createRoot( editingView.document ); view = new DecoupledEditorUIView( locale, editingView ); view.editable.name = editingViewRoot.rootName; } ); @@ -130,11 +131,4 @@ describe( 'DecoupledEditorUIView', () => { view.editable.element.remove(); } ); } ); - - function setUpEditingView() { - editingView = new EditingView(); - editingViewRoot = new ViewRootEditableElement( 'div' ); - editingViewRoot._document = editingView.document; - editingView.document.roots.add( editingViewRoot ); - } } ); From 0728249ebf4f520158254cf359b710862a798774 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 7 Feb 2019 15:42:45 +0100 Subject: [PATCH 17/17] =?UTF-8?q?Tests:=20Added=20UI=20tests=20that=20chec?= =?UTF-8?q?k=20the=20UI=20destruction=20order=20and=20editor=20element=20a?= =?UTF-8?q?ttributes=20clean=E2=80=93up.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/decouplededitorui.js | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/decouplededitorui.js b/tests/decouplededitorui.js index 540737d..c0e4eec 100644 --- a/tests/decouplededitorui.js +++ b/tests/decouplededitorui.js @@ -195,6 +195,47 @@ describe( 'DecoupledEditorUI', () => { } ); } ); + describe( 'destroy()', () => { + it( 'detaches the DOM root then destroys the UI view', () => { + return VirtualDecoupledTestEditor.create( '' ) + .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 VirtualDecoupledTestEditor.create( domElement ) + .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 );