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 #533 from ckeditor/context
Browse files Browse the repository at this point in the history
Feature: Reintroduced the concept of body collections with a focus on better management of multiple editors and support for context plugins (plugins which leave outside an editor instance). Closes ckeditor/ckeditor5#5888.
  • Loading branch information
Reinmar authored Jan 16, 2020
2 parents a1f009b + 3df5493 commit 23d12e9
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 71 deletions.
88 changes: 88 additions & 0 deletions src/editorui/bodycollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/**
* @module ui/editorui/bodycollection
*/

/* globals document */

import Template from '../template';
import ViewCollection from '../viewcollection';

import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';

/**
* This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached
* from the DOM structure of the editor, like panels, icons, etc.
*
* The body collection is available in the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
* Any plugin can add a {@link module:ui/view~View view} to this collection.
* Those views will render in a container placed directly in the `<body>` element.
* The editor will detach and destroy this collection when the editor will be {@link module:core/editor/editor~Editor#destroy destroyed}.
*
* If you need to control the life cycle of the body collection on your own, you can create your own instance of this class.
*
* A body collection will render itself automatically in the DOM body element as soon as you call {@link ~BodyCollection#attachToDom}.
* If you create multiple body collections this class will create a special wrapper element in the DOM to limit the number of
* elements created directly in the body and remove it when the last body collection will be
* {@link ~BodyCollection#detachFromDom detached}.
*
* @extends module:ui/viewcollection~ViewCollection
*/
export default class BodyCollection extends ViewCollection {
/**
* Attaches the body collection to the DOM body element. You need to execute this method to render the content of
* the body collection.
*/
attachToDom() {
/**
* The element holding elements of the body region.
*
* @protected
* @member {HTMLElement} #_bodyCollectionContainer
*/
this._bodyCollectionContainer = new Template( {
tag: 'div',
attributes: {
class: [
'ck',
'ck-reset_all',
'ck-body',
'ck-rounded-corners'
],
dir: this.locale.uiLanguageDirection,
},
children: this
} ).render();

let wrapper = document.querySelector( '.ck-body-wrapper' );

if ( !wrapper ) {
wrapper = createElement( document, 'div', { class: 'ck-body-wrapper' } );
document.body.appendChild( wrapper );
}

wrapper.appendChild( this._bodyCollectionContainer );
}

/**
* Detach the collection from the DOM structure. Use this method when you do not need to use the body collection
* anymore to clean-up the DOM structure.
*/
detachFromDom() {
super.destroy();

if ( this._bodyCollectionContainer ) {
this._bodyCollectionContainer.remove();
}

const wrapper = document.querySelector( '.ck-body-wrapper' );

if ( wrapper && wrapper.childElementCount == 0 ) {
wrapper.remove();
}
}
}
41 changes: 4 additions & 37 deletions src/editorui/editoruiview.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
* @module ui/editorui/editoruiview
*/

/* globals document */

import View from '../view';
import Template from '../template';
import BodyCollection from './bodycollection';

import '../../theme/components/editorui/editorui.css';

Expand All @@ -35,14 +33,7 @@ export default class EditorUIView extends View {
* @readonly
* @member {module:ui/viewcollection~ViewCollection} #body
*/
this.body = this.createCollection();

/**
* The element holding elements of the 'body' region.
*
* @private
* @member {HTMLElement} #_bodyCollectionContainer
*/
this.body = new BodyCollection( locale );
}

/**
Expand All @@ -51,39 +42,15 @@ export default class EditorUIView extends View {
render() {
super.render();

this._renderBodyCollection();
this.body.attachToDom();
}

/**
* @inheritDoc
*/
destroy() {
this._bodyCollectionContainer.remove();
this.body.detachFromDom();

return super.destroy();
}

/**
* Creates and appends to `<body>` the {@link #body} collection container.
*
* @private
*/
_renderBodyCollection() {
const locale = this.locale;
const bodyElement = this._bodyCollectionContainer = new Template( {
tag: 'div',
attributes: {
class: [
'ck',
'ck-reset_all',
'ck-body',
'ck-rounded-corners'
],
dir: locale.uiLanguageDirection,
},
children: this.body
} ).render();

document.body.appendChild( bodyElement );
}
}
6 changes: 3 additions & 3 deletions src/notification/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

/* globals window */

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ContextPlugin from '@ckeditor/ckeditor5-core/src/contextplugin';

/**
* The Notification plugin.
Expand All @@ -21,9 +21,9 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
* Note that every unhandled and not stopped `warning` notification will be displayed as a system alert.
* See {@link module:ui/notification/notification~Notification#showWarning}.
*
* @extends module:core/plugin~Plugin
* @extends module:core/contextplugin~ContextPlugin
*/
export default class Notification extends Plugin {
export default class Notification extends ContextPlugin {
/**
* @inheritDoc
*/
Expand Down
186 changes: 186 additions & 0 deletions tests/editorui/bodycollection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* global document */

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

import BodyCollection from '../../src/editorui/bodycollection';
import View from '../../src/view';

describe( 'BodyCollection', () => {
let locale;

testUtils.createSinonSandbox();

beforeEach( () => {
locale = new Locale();
} );

afterEach( () => {
const wrappers = Array.from( document.querySelectorAll( '.ck-body-wrapper' ) );

for ( const wrapper of wrappers ) {
wrapper.remove();
}
} );

describe( 'attachToDom', () => {
it( 'should create wrapper and put the collection in that wrapper', () => {
const body = new BodyCollection( locale );

body.attachToDom();

const wrappers = Array.from( document.querySelectorAll( '.ck-body-wrapper' ) );

expect( wrappers.length ).to.equal( 1 );
expect( wrappers[ 0 ].parentNode ).to.equal( document.body );

const el = body._bodyCollectionContainer;

expect( el.parentNode ).to.equal( wrappers[ 0 ] );
expect( el.classList.contains( 'ck' ) ).to.be.true;
expect( el.classList.contains( 'ck-body' ) ).to.be.true;
expect( el.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
expect( el.classList.contains( 'ck-reset_all' ) ).to.be.true;
} );

it( 'sets the right dir attribute to the body region (LTR)', () => {
const body = new BodyCollection( locale );

body.attachToDom();

const el = body._bodyCollectionContainer;

expect( el.getAttribute( 'dir' ) ).to.equal( 'ltr' );
} );

it( 'sets the right dir attribute to the body region (RTL)', () => {
const locale = new Locale( { uiLanguage: 'ar' } );
const body = new BodyCollection( locale );

body.attachToDom();

const el = body._bodyCollectionContainer;

expect( el.getAttribute( 'dir' ) ).to.equal( 'rtl' );
} );

it( 'should put all body elements to the same wrapper', () => {
const body1 = new BodyCollection( locale );
body1.attachToDom();

expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 1 );

const body2 = new BodyCollection( locale );
body2.attachToDom();

const bodyElements = document.querySelectorAll( '.ck-body' );

expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
expect( bodyElements.length ).to.equal( 2 );
expect( bodyElements[ 0 ].parentNode ).to.equal( bodyElements[ 1 ].parentNode );
} );

it( 'should render views in proper body collections', () => {
const body1 = new BodyCollection( locale );

const view1 = new View();
view1.setTemplate( {
tag: 'div',
attributes: {
class: [ 'foo' ]
}
} );

// Should work if body is attached before the view is added...
body1.attachToDom();
body1.add( view1 );

const body2 = new BodyCollection( locale );

const view2 = new View();
view2.setTemplate( {
tag: 'div',
attributes: {
class: [ 'bar' ]
}
} );

// ...and it should work if body is attached after the view is added.
body2.add( view2 );
body2.attachToDom();

const wrappers = Array.from( document.querySelectorAll( '.ck-body-wrapper' ) );

expect( wrappers.length ).to.equal( 1 );

const wrapper = wrappers[ 0 ];
const body1Element = body1._bodyCollectionContainer;
const body2Element = body2._bodyCollectionContainer;

expect( body1Element.parentNode ).to.equal( wrapper );
expect( body1Element.childNodes.length ).to.equal( 1 );
expect( body1Element.childNodes[ 0 ].classList.contains( 'foo' ) ).to.be.true;

expect( body2Element.parentNode ).to.equal( wrapper );
expect( body2Element.childNodes.length ).to.equal( 1 );
expect( body2Element.childNodes[ 0 ].classList.contains( 'bar' ) ).to.be.true;
} );
} );

describe( 'detachFromDom', () => {
it( 'removes the body collection from DOM', () => {
const body = new BodyCollection( locale );

body.attachToDom();
body.detachFromDom();

expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 0 );
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 0 );
} );

it( 'removes the multiple body collections from dom and remove the wrapper when the last is removed', () => {
const body1 = new BodyCollection( locale );
body1.attachToDom();

const body2 = new BodyCollection( locale );
body2.attachToDom();

expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 2 );

body1.detachFromDom();

expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 1 );

body2.detachFromDom();

expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 0 );
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 0 );
} );

it( 'should not throw when be called multiple times', () => {
const body = new BodyCollection( locale );
body.attachToDom();

expect( () => {
body.detachFromDom();
body.detachFromDom();
} ).to.not.throw();
} );

it( 'should not throw if attachToDom was not called before', () => {
const body = new BodyCollection( locale );

expect( () => {
body.detachFromDom();
} ).to.not.throw();
} );
} );
} );
Loading

0 comments on commit 23d12e9

Please sign in to comment.