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

Commit 23d12e9

Browse files
authored
Merge pull request #533 from ckeditor/context
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.
2 parents a1f009b + 3df5493 commit 23d12e9

File tree

6 files changed

+289
-71
lines changed

6 files changed

+289
-71
lines changed

src/editorui/bodycollection.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/**
7+
* @module ui/editorui/bodycollection
8+
*/
9+
10+
/* globals document */
11+
12+
import Template from '../template';
13+
import ViewCollection from '../viewcollection';
14+
15+
import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
16+
17+
/**
18+
* This is a special {@link module:ui/viewcollection~ViewCollection} dedicated to elements that are detached
19+
* from the DOM structure of the editor, like panels, icons, etc.
20+
*
21+
* The body collection is available in the {@link module:ui/editorui/editoruiview~EditorUIView#body `editor.ui.view.body`} property.
22+
* Any plugin can add a {@link module:ui/view~View view} to this collection.
23+
* Those views will render in a container placed directly in the `<body>` element.
24+
* The editor will detach and destroy this collection when the editor will be {@link module:core/editor/editor~Editor#destroy destroyed}.
25+
*
26+
* If you need to control the life cycle of the body collection on your own, you can create your own instance of this class.
27+
*
28+
* A body collection will render itself automatically in the DOM body element as soon as you call {@link ~BodyCollection#attachToDom}.
29+
* If you create multiple body collections this class will create a special wrapper element in the DOM to limit the number of
30+
* elements created directly in the body and remove it when the last body collection will be
31+
* {@link ~BodyCollection#detachFromDom detached}.
32+
*
33+
* @extends module:ui/viewcollection~ViewCollection
34+
*/
35+
export default class BodyCollection extends ViewCollection {
36+
/**
37+
* Attaches the body collection to the DOM body element. You need to execute this method to render the content of
38+
* the body collection.
39+
*/
40+
attachToDom() {
41+
/**
42+
* The element holding elements of the body region.
43+
*
44+
* @protected
45+
* @member {HTMLElement} #_bodyCollectionContainer
46+
*/
47+
this._bodyCollectionContainer = new Template( {
48+
tag: 'div',
49+
attributes: {
50+
class: [
51+
'ck',
52+
'ck-reset_all',
53+
'ck-body',
54+
'ck-rounded-corners'
55+
],
56+
dir: this.locale.uiLanguageDirection,
57+
},
58+
children: this
59+
} ).render();
60+
61+
let wrapper = document.querySelector( '.ck-body-wrapper' );
62+
63+
if ( !wrapper ) {
64+
wrapper = createElement( document, 'div', { class: 'ck-body-wrapper' } );
65+
document.body.appendChild( wrapper );
66+
}
67+
68+
wrapper.appendChild( this._bodyCollectionContainer );
69+
}
70+
71+
/**
72+
* Detach the collection from the DOM structure. Use this method when you do not need to use the body collection
73+
* anymore to clean-up the DOM structure.
74+
*/
75+
detachFromDom() {
76+
super.destroy();
77+
78+
if ( this._bodyCollectionContainer ) {
79+
this._bodyCollectionContainer.remove();
80+
}
81+
82+
const wrapper = document.querySelector( '.ck-body-wrapper' );
83+
84+
if ( wrapper && wrapper.childElementCount == 0 ) {
85+
wrapper.remove();
86+
}
87+
}
88+
}

src/editorui/editoruiview.js

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
* @module ui/editorui/editoruiview
88
*/
99

10-
/* globals document */
11-
1210
import View from '../view';
13-
import Template from '../template';
11+
import BodyCollection from './bodycollection';
1412

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

@@ -35,14 +33,7 @@ export default class EditorUIView extends View {
3533
* @readonly
3634
* @member {module:ui/viewcollection~ViewCollection} #body
3735
*/
38-
this.body = this.createCollection();
39-
40-
/**
41-
* The element holding elements of the 'body' region.
42-
*
43-
* @private
44-
* @member {HTMLElement} #_bodyCollectionContainer
45-
*/
36+
this.body = new BodyCollection( locale );
4637
}
4738

4839
/**
@@ -51,39 +42,15 @@ export default class EditorUIView extends View {
5142
render() {
5243
super.render();
5344

54-
this._renderBodyCollection();
45+
this.body.attachToDom();
5546
}
5647

5748
/**
5849
* @inheritDoc
5950
*/
6051
destroy() {
61-
this._bodyCollectionContainer.remove();
52+
this.body.detachFromDom();
6253

6354
return super.destroy();
6455
}
65-
66-
/**
67-
* Creates and appends to `<body>` the {@link #body} collection container.
68-
*
69-
* @private
70-
*/
71-
_renderBodyCollection() {
72-
const locale = this.locale;
73-
const bodyElement = this._bodyCollectionContainer = new Template( {
74-
tag: 'div',
75-
attributes: {
76-
class: [
77-
'ck',
78-
'ck-reset_all',
79-
'ck-body',
80-
'ck-rounded-corners'
81-
],
82-
dir: locale.uiLanguageDirection,
83-
},
84-
children: this.body
85-
} ).render();
86-
87-
document.body.appendChild( bodyElement );
88-
}
8956
}

src/notification/notification.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
/* globals window */
1111

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

1414
/**
1515
* The Notification plugin.
@@ -21,9 +21,9 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
2121
* Note that every unhandled and not stopped `warning` notification will be displayed as a system alert.
2222
* See {@link module:ui/notification/notification~Notification#showWarning}.
2323
*
24-
* @extends module:core/plugin~Plugin
24+
* @extends module:core/contextplugin~ContextPlugin
2525
*/
26-
export default class Notification extends Plugin {
26+
export default class Notification extends ContextPlugin {
2727
/**
2828
* @inheritDoc
2929
*/

tests/editorui/bodycollection.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4+
*/
5+
6+
/* global document */
7+
8+
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
9+
import Locale from '@ckeditor/ckeditor5-utils/src/locale';
10+
11+
import BodyCollection from '../../src/editorui/bodycollection';
12+
import View from '../../src/view';
13+
14+
describe( 'BodyCollection', () => {
15+
let locale;
16+
17+
testUtils.createSinonSandbox();
18+
19+
beforeEach( () => {
20+
locale = new Locale();
21+
} );
22+
23+
afterEach( () => {
24+
const wrappers = Array.from( document.querySelectorAll( '.ck-body-wrapper' ) );
25+
26+
for ( const wrapper of wrappers ) {
27+
wrapper.remove();
28+
}
29+
} );
30+
31+
describe( 'attachToDom', () => {
32+
it( 'should create wrapper and put the collection in that wrapper', () => {
33+
const body = new BodyCollection( locale );
34+
35+
body.attachToDom();
36+
37+
const wrappers = Array.from( document.querySelectorAll( '.ck-body-wrapper' ) );
38+
39+
expect( wrappers.length ).to.equal( 1 );
40+
expect( wrappers[ 0 ].parentNode ).to.equal( document.body );
41+
42+
const el = body._bodyCollectionContainer;
43+
44+
expect( el.parentNode ).to.equal( wrappers[ 0 ] );
45+
expect( el.classList.contains( 'ck' ) ).to.be.true;
46+
expect( el.classList.contains( 'ck-body' ) ).to.be.true;
47+
expect( el.classList.contains( 'ck-rounded-corners' ) ).to.be.true;
48+
expect( el.classList.contains( 'ck-reset_all' ) ).to.be.true;
49+
} );
50+
51+
it( 'sets the right dir attribute to the body region (LTR)', () => {
52+
const body = new BodyCollection( locale );
53+
54+
body.attachToDom();
55+
56+
const el = body._bodyCollectionContainer;
57+
58+
expect( el.getAttribute( 'dir' ) ).to.equal( 'ltr' );
59+
} );
60+
61+
it( 'sets the right dir attribute to the body region (RTL)', () => {
62+
const locale = new Locale( { uiLanguage: 'ar' } );
63+
const body = new BodyCollection( locale );
64+
65+
body.attachToDom();
66+
67+
const el = body._bodyCollectionContainer;
68+
69+
expect( el.getAttribute( 'dir' ) ).to.equal( 'rtl' );
70+
} );
71+
72+
it( 'should put all body elements to the same wrapper', () => {
73+
const body1 = new BodyCollection( locale );
74+
body1.attachToDom();
75+
76+
expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
77+
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 1 );
78+
79+
const body2 = new BodyCollection( locale );
80+
body2.attachToDom();
81+
82+
const bodyElements = document.querySelectorAll( '.ck-body' );
83+
84+
expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
85+
expect( bodyElements.length ).to.equal( 2 );
86+
expect( bodyElements[ 0 ].parentNode ).to.equal( bodyElements[ 1 ].parentNode );
87+
} );
88+
89+
it( 'should render views in proper body collections', () => {
90+
const body1 = new BodyCollection( locale );
91+
92+
const view1 = new View();
93+
view1.setTemplate( {
94+
tag: 'div',
95+
attributes: {
96+
class: [ 'foo' ]
97+
}
98+
} );
99+
100+
// Should work if body is attached before the view is added...
101+
body1.attachToDom();
102+
body1.add( view1 );
103+
104+
const body2 = new BodyCollection( locale );
105+
106+
const view2 = new View();
107+
view2.setTemplate( {
108+
tag: 'div',
109+
attributes: {
110+
class: [ 'bar' ]
111+
}
112+
} );
113+
114+
// ...and it should work if body is attached after the view is added.
115+
body2.add( view2 );
116+
body2.attachToDom();
117+
118+
const wrappers = Array.from( document.querySelectorAll( '.ck-body-wrapper' ) );
119+
120+
expect( wrappers.length ).to.equal( 1 );
121+
122+
const wrapper = wrappers[ 0 ];
123+
const body1Element = body1._bodyCollectionContainer;
124+
const body2Element = body2._bodyCollectionContainer;
125+
126+
expect( body1Element.parentNode ).to.equal( wrapper );
127+
expect( body1Element.childNodes.length ).to.equal( 1 );
128+
expect( body1Element.childNodes[ 0 ].classList.contains( 'foo' ) ).to.be.true;
129+
130+
expect( body2Element.parentNode ).to.equal( wrapper );
131+
expect( body2Element.childNodes.length ).to.equal( 1 );
132+
expect( body2Element.childNodes[ 0 ].classList.contains( 'bar' ) ).to.be.true;
133+
} );
134+
} );
135+
136+
describe( 'detachFromDom', () => {
137+
it( 'removes the body collection from DOM', () => {
138+
const body = new BodyCollection( locale );
139+
140+
body.attachToDom();
141+
body.detachFromDom();
142+
143+
expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 0 );
144+
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 0 );
145+
} );
146+
147+
it( 'removes the multiple body collections from dom and remove the wrapper when the last is removed', () => {
148+
const body1 = new BodyCollection( locale );
149+
body1.attachToDom();
150+
151+
const body2 = new BodyCollection( locale );
152+
body2.attachToDom();
153+
154+
expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
155+
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 2 );
156+
157+
body1.detachFromDom();
158+
159+
expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 1 );
160+
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 1 );
161+
162+
body2.detachFromDom();
163+
164+
expect( document.querySelectorAll( '.ck-body-wrapper' ).length ).to.equal( 0 );
165+
expect( document.querySelectorAll( '.ck-body' ).length ).to.equal( 0 );
166+
} );
167+
168+
it( 'should not throw when be called multiple times', () => {
169+
const body = new BodyCollection( locale );
170+
body.attachToDom();
171+
172+
expect( () => {
173+
body.detachFromDom();
174+
body.detachFromDom();
175+
} ).to.not.throw();
176+
} );
177+
178+
it( 'should not throw if attachToDom was not called before', () => {
179+
const body = new BodyCollection( locale );
180+
181+
expect( () => {
182+
body.detachFromDom();
183+
} ).to.not.throw();
184+
} );
185+
} );
186+
} );

0 commit comments

Comments
 (0)