Selaa lähdekoodia

Merge pull request #65 from ckeditor/i/5597

Feature: The inline editor toolbar should group items when its width exceeds the editable’s width (see ckeditor/ckeditor5#5597).

BREAKING CHANGE: From now on, the inline toolbar groups overflowing items by default. This behavior can be disabled via the editor config by setting the [`config.toolbar.shouldNotGroupWhenFull: true`](https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html#member-toolbar) option.
Aleksander Nowodzinski 5 vuotta sitten
vanhempi
sitoutus
fe151b0f35

+ 5 - 1
packages/ckeditor5-editor-inline/src/inlineeditor.js

@@ -73,7 +73,11 @@ export default class InlineEditor extends Editor {
 			secureSourceElement( this );
 		}
 
-		const view = new InlineEditorUIView( this.locale, this.editing.view, this.sourceElement );
+		const shouldToolbarGroupWhenFull = !this.config.get( 'toolbar.shouldNotGroupWhenFull' );
+
+		const view = new InlineEditorUIView( this.locale, this.editing.view, this.sourceElement, {
+			shouldToolbarGroupWhenFull
+		} );
 		this.ui = new InlineEditorUI( this, view );
 
 		attachToForm( this );

+ 47 - 2
packages/ckeditor5-editor-inline/src/inlineeditoruiview.js

@@ -11,6 +11,11 @@ import EditorUIView from '@ckeditor/ckeditor5-ui/src/editorui/editoruiview';
 import InlineEditableUIView from '@ckeditor/ckeditor5-ui/src/editableui/inline/inlineeditableuiview';
 import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
 import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
+import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
+
+const toPx = toUnit( 'px' );
 
 /**
  * Inline editor UI view. Uses an nline editable and a floating toolbar.
@@ -25,8 +30,12 @@ export default class InlineEditorUIView extends EditorUIView {
 	 * @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.
+	 * @param {Object} [options={}] Configuration options for the view instance.
+	 * @param {Boolean} [options.shouldToolbarGroupWhenFull] When set `true` enables automatic items grouping
+	 * in the main {@link module:editor-inline/inlineeditoruiview~InlineEditorUIView#toolbar toolbar}.
+	 * See {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} to learn more.
 	 */
-	constructor( locale, editingView, editableElement ) {
+	constructor( locale, editingView, editableElement, options = {} ) {
 		super( locale );
 
 		/**
@@ -35,7 +44,9 @@ export default class InlineEditorUIView extends EditorUIView {
 		 * @readonly
 		 * @member {module:ui/toolbar/toolbarview~ToolbarView}
 		 */
-		this.toolbar = new ToolbarView( locale );
+		this.toolbar = new ToolbarView( locale, {
+			shouldGroupWhenFull: options.shouldToolbarGroupWhenFull
+		} );
 
 		/**
 		 * The offset from the top edge of the web browser's viewport which makes the
@@ -133,6 +144,17 @@ export default class InlineEditorUIView extends EditorUIView {
 		 * @member {module:ui/editableui/inline/inlineeditableuiview~InlineEditableUIView}
 		 */
 		this.editable = new InlineEditableUIView( locale, editingView, editableElement );
+
+		/**
+		 * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
+		 * and manage items that do not fit into a single row.
+		 *
+		 * **Note:** Created in {@link #render}.
+		 *
+		 * @private
+		 * @member {module:utils/dom/resizeobserver~ResizeObserver}
+		 */
+		this._resizeObserver = null;
 	}
 
 	/**
@@ -144,6 +166,29 @@ export default class InlineEditorUIView extends EditorUIView {
 		this.body.add( this.panel );
 		this.registerChild( this.editable );
 		this.panel.content.add( this.toolbar );
+
+		const options = this.toolbar.options;
+
+		// Set toolbar's max-width on the initialization and update it on the editable resize,
+		// if 'shouldToolbarGroupWhenFull' in config is set to 'true'.
+		if ( options.shouldGroupWhenFull ) {
+			const editableElement = this.editable.element;
+
+			this._resizeObserver = new ResizeObserver( editableElement, () => {
+				this.toolbar.maxWidth = toPx( new Rect( editableElement ).width );
+			} );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		super.destroy();
+
+		if ( this._resizeObserver ) {
+			this._resizeObserver.destroy();
+		}
 	}
 
 	/**

+ 16 - 0
packages/ckeditor5-editor-inline/tests/inlineeditor.js

@@ -183,6 +183,22 @@ describe( 'InlineEditor', () => {
 			} );
 		} );
 
+		it( 'should pass the config.toolbar.shouldNotGroupWhenFull configuration to the view', () => {
+			const editorElement = document.createElement( 'div' );
+
+			return InlineEditor.create( editorElement, {
+				toolbar: {
+					shouldNotGroupWhenFull: true
+				}
+			} ).then( editor => {
+				expect( editor.ui.view.toolbar.options.shouldGroupWhenFull ).to.be.false;
+
+				return editor.destroy();
+			} ).then( () => {
+				editorElement.remove();
+			} );
+		} );
+
 		it( 'throws if initial data is passed in Editor#create and config.initialData is also used', done => {
 			InlineEditor.create( '<p>Hello world!</p>', {
 				initialData: '<p>I am evil!</p>',

+ 9 - 5
packages/ckeditor5-editor-inline/tests/inlineeditorui.js

@@ -86,11 +86,6 @@ describe( 'InlineEditorUI', () => {
 			it( 'pin() is called on editor.ui#update', () => {
 				const spy = sinon.stub( view.panel, 'pin' );
 
-				view.panel.hide();
-
-				editor.ui.fire( 'update' );
-				sinon.assert.notCalled( spy );
-
 				view.panel.show();
 
 				editor.ui.fire( 'update' );
@@ -100,6 +95,15 @@ describe( 'InlineEditorUI', () => {
 					positions: sinon.match.array
 				} );
 			} );
+
+			it( 'pin() is not called on editor.ui#update when panel is hidden', () => {
+				const spy = sinon.stub( view.panel, 'pin' );
+
+				view.panel.hide();
+
+				editor.ui.fire( 'update' );
+				sinon.assert.notCalled( spy );
+			} );
 		} );
 
 		describe( 'editable', () => {

+ 77 - 4
packages/ckeditor5-editor-inline/tests/inlineeditoruiview.js

@@ -10,11 +10,18 @@ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpa
 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 Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
+
+const toPx = toUnit( 'px' );
 
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 describe( 'InlineEditorUIView', () => {
 	let locale, view, editingView, editingViewRoot;
+	let resizeCallback;
 
 	testUtils.createSinonSandbox();
 
@@ -24,6 +31,24 @@ describe( 'InlineEditorUIView', () => {
 		editingViewRoot = createRoot( editingView.document );
 		view = new InlineEditorUIView( locale, editingView );
 		view.editable.name = editingViewRoot.rootName;
+
+		// Make sure other tests of the editor do not affect tests that follow.
+		// Without it, if an instance of ResizeObserver already exists somewhere undestroyed
+		// in DOM, the following DOM mock will have no effect.
+		ResizeObserver._observerInstance = null;
+
+		testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => {
+			resizeCallback = callback;
+
+			return {
+				observe: sinon.spy(),
+				unobserve: sinon.spy()
+			};
+		} );
+	} );
+
+	afterEach( () => {
+		view.destroy();
 	} );
 
 	describe( 'constructor()', () => {
@@ -39,6 +64,16 @@ describe( 'InlineEditorUIView', () => {
 			it( 'is not rendered', () => {
 				expect( view.toolbar.isRendered ).to.be.false;
 			} );
+
+			it( 'should have the shouldGroupWhenFull option set based on constructor options', () => {
+				const view = new InlineEditorUIView( locale, editingView, null, {
+					shouldToolbarGroupWhenFull: true
+				} );
+
+				expect( view.toolbar.options.shouldGroupWhenFull ).to.be.true;
+
+				view.destroy();
+			} );
 		} );
 
 		describe( '#panel', () => {
@@ -79,10 +114,6 @@ describe( 'InlineEditorUIView', () => {
 			view.render();
 		} );
 
-		afterEach( () => {
-			view.destroy();
-		} );
-
 		describe( '#toolbar', () => {
 			it( 'is given the right CSS classes', () => {
 				expect( view.toolbar.element.classList.contains( 'ck-toolbar_floating' ) ).to.be.true;
@@ -91,6 +122,48 @@ describe( 'InlineEditorUIView', () => {
 			it( 'sets the default value of the #viewportTopOffset attribute', () => {
 				expect( view.viewportTopOffset ).to.equal( 0 );
 			} );
+
+			describe( 'automatic resizing when shouldToolbarGroupWhenFull is "true"', () => {
+				it( 'should set and update toolbar max-width according to the width of the editable element', () => {
+					const locale = new Locale();
+					const editingView = new EditingView();
+					const editingViewRoot = createRoot( editingView.document );
+					const view = new InlineEditorUIView( locale, editingView, null, {
+						shouldToolbarGroupWhenFull: true
+					} );
+					view.editable.name = editingViewRoot.rootName;
+					view.render();
+
+					const editableElement = view.editable.element;
+
+					// View element should be inside the body, otherwise the `Rect` instance will complain
+					// that it's not available in the DOM.
+					global.document.body.appendChild( editableElement );
+
+					editableElement.style.width = '400px';
+
+					resizeCallback( [ {
+						target: editableElement,
+						contentRect: new Rect( editableElement )
+					} ] );
+
+					// Include paddings.
+					expect( view.toolbar.maxWidth ).to.be.equal( toPx( new Rect( editableElement ).width ) );
+
+					editableElement.style.width = '200px';
+
+					resizeCallback( [ {
+						target: editableElement,
+						contentRect: new Rect( editableElement )
+					} ] );
+
+					// Include paddings.
+					expect( view.toolbar.maxWidth ).to.be.equal( toPx( new Rect( editableElement ).width ) );
+
+					editableElement.remove();
+					view.destroy();
+				} );
+			} );
 		} );
 
 		describe( '#panel', () => {