8
0
Просмотр исходного кода

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.
Piotrek Koszuliński 6 лет назад
Родитель
Сommit
474093ef55

+ 88 - 0
packages/ckeditor5-ui/src/editorui/bodycollection.js

@@ -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();
+		}
+	}
+}

+ 4 - 37
packages/ckeditor5-ui/src/editorui/editoruiview.js

@@ -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';
 
@@ -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 );
 	}
 
 	/**
@@ -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 );
-	}
 }

+ 3 - 3
packages/ckeditor5-ui/src/notification/notification.js

@@ -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.
@@ -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
 	 */

+ 186 - 0
packages/ckeditor5-ui/tests/editorui/bodycollection.js

@@ -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();
+		} );
+	} );
+} );

+ 6 - 29
packages/ckeditor5-ui/tests/editorui/editoruiview.js

@@ -37,41 +37,18 @@ describe( 'EditorUIView', () => {
 	} );
 
 	describe( 'render()', () => {
-		it( 'sets the right class set to the body region', () => {
-			const el = view._bodyCollectionContainer;
-
-			expect( el.parentNode ).to.equal( document.body );
-			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 el = view._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 view = new EditorUIView( locale );
-
-			view.render();
-
-			const el = view._bodyCollectionContainer;
-
-			expect( el.getAttribute( 'dir' ) ).to.equal( 'rtl' );
-
-			view.destroy();
+		it( 'attach the body collection', () => {
+			expect( view.body._bodyCollectionContainer.parentNode.classList.contains( 'ck-body-wrapper' ) ).to.be.true;
+			expect( view.body._bodyCollectionContainer.parentNode.parentNode ).to.equal( document.body );
 		} );
 	} );
 
 	describe( 'destroy()', () => {
-		it( 'removes the body region container', () => {
-			const el = view._bodyCollectionContainer;
+		it( 'detach the body collection', () => {
+			const el = view.body._bodyCollectionContainer;
 
 			view.destroy();
+
 			expect( el.parentNode ).to.be.null;
 		} );
 

+ 2 - 2
packages/ckeditor5-ui/tests/notification/notification.js

@@ -7,7 +7,7 @@
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ContextPlugin from '@ckeditor/ckeditor5-core/src/contextplugin';
 import Notification from '../../src/notification/notification';
 
 describe( 'Notification', () => {
@@ -29,7 +29,7 @@ describe( 'Notification', () => {
 	describe( 'init()', () => {
 		it( 'should create notification plugin', () => {
 			expect( notification ).to.instanceof( Notification );
-			expect( notification ).to.instanceof( Plugin );
+			expect( notification ).to.instanceof( ContextPlugin );
 		} );
 	} );