8
0
Prechádzať zdrojové kódy

Make block toolbar groupable

panr 5 rokov pred
rodič
commit
1ed9005f2c

+ 64 - 3
packages/ckeditor5-ui/src/toolbar/block/blocktoolbar.js

@@ -19,9 +19,15 @@ import clickOutsideHandler from '../../bindings/clickoutsidehandler';
 
 
 import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
 import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import normalizeToolbarConfig from '../normalizetoolbarconfig';
 
 
+import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
+
+import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
 import iconPilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
 import iconPilcrow from '@ckeditor/ckeditor5-core/theme/icons/pilcrow.svg';
 
 
+const toPx = toUnit( 'px' );
+
 /**
 /**
  * The block toolbar plugin.
  * The block toolbar plugin.
  *
  *
@@ -77,6 +83,14 @@ export default class BlockToolbar extends Plugin {
 	constructor( editor ) {
 	constructor( editor ) {
 		super( editor );
 		super( editor );
 
 
+		/**
+		 * A cached and normalized `config.blockToolbar` object.
+		 *
+		 * @type {module:core/editor/editorconfig~EditorConfig#blockToolbar}
+		 * @private
+		 */
+		this._blockToolbarConfig = normalizeToolbarConfig( this.editor.config.get( 'blockToolbar' ) );
+
 		/**
 		/**
 		 * The toolbar view.
 		 * The toolbar view.
 		 *
 		 *
@@ -98,6 +112,20 @@ export default class BlockToolbar extends Plugin {
 		 */
 		 */
 		this.buttonView = this._createButtonView();
 		this.buttonView = this._createButtonView();
 
 
+		/**
+		 * An instance of the resize observer that allows to respond to changes in editable's geometry
+		 * so the toolbar can stay within its boundaries (and group toolbar items that do not fit).
+		 *
+		 * **Note**: Used only when `shouldNotGroupWhenFull` was **not** set in the
+		 * {@link module:core/editor/editorconfig~EditorConfig#blockToolbar configuration}.
+		 *
+		 * **Note:** Created in {@link #afterInit}.
+		 *
+		 * @protected
+		 * @member {module:utils/dom/resizeobserver~ResizeObserver}
+		 */
+		this._resizeObserver = null;
+
 		// Close the #panelView upon clicking outside of the plugin UI.
 		// Close the #panelView upon clicking outside of the plugin UI.
 		clickOutsideHandler( {
 		clickOutsideHandler( {
 			emitter: this.panelView,
 			emitter: this.panelView,
@@ -149,14 +177,25 @@ export default class BlockToolbar extends Plugin {
 	 */
 	 */
 	afterInit() {
 	afterInit() {
 		const factory = this.editor.ui.componentFactory;
 		const factory = this.editor.ui.componentFactory;
-		const config = this.editor.config.get( 'blockToolbar' ) || [];
+		const config = this._blockToolbarConfig;
 
 
-		this.toolbarView.fillFromConfig( config, factory );
+		this.toolbarView.fillFromConfig( config.items, factory );
 
 
 		// Hide panel before executing each button in the panel.
 		// Hide panel before executing each button in the panel.
 		for ( const item of this.toolbarView.items ) {
 		for ( const item of this.toolbarView.items ) {
 			item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } );
 			item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } );
 		}
 		}
+
+		if ( !config.shouldNotGroupWhenFull ) {
+			this.listenTo( this.editor, 'ready', () => {
+				const editableElement = this.editor.ui.view.editable.element;
+
+				// Set #toolbarView's max-width just after the initialization and update it on the editable resize.
+				this._resizeObserver = new ResizeObserver( editableElement, () => {
+					this.toolbarView.maxWidth = this._getToolbarMaxWidth();
+				} );
+			} );
+		}
 	}
 	}
 
 
 	/**
 	/**
@@ -178,7 +217,10 @@ export default class BlockToolbar extends Plugin {
 	 * @returns {module:ui/toolbar/toolbarview~ToolbarView}
 	 * @returns {module:ui/toolbar/toolbarview~ToolbarView}
 	 */
 	 */
 	_createToolbarView() {
 	_createToolbarView() {
-		const toolbarView = new ToolbarView( this.editor.locale );
+		const shouldGroupWhenFull = !this._blockToolbarConfig.shouldNotGroupWhenFull;
+		const toolbarView = new ToolbarView( this.editor.locale, {
+			shouldGroupWhenFull
+		} );
 
 
 		toolbarView.extendTemplate( {
 		toolbarView.extendTemplate( {
 			attributes: {
 			attributes: {
@@ -333,6 +375,8 @@ export default class BlockToolbar extends Plugin {
 		if ( !wasVisible ) {
 		if ( !wasVisible ) {
 			this.toolbarView.items.get( 0 ).focus();
 			this.toolbarView.items.get( 0 ).focus();
 		}
 		}
+
+		this.toolbarView.maxWidth = this._getToolbarMaxWidth();
 	}
 	}
 
 
 	/**
 	/**
@@ -388,6 +432,23 @@ export default class BlockToolbar extends Plugin {
 		this.buttonView.top = position.top;
 		this.buttonView.top = position.top;
 		this.buttonView.left = position.left;
 		this.buttonView.left = position.left;
 	}
 	}
+
+	/**
+	 * Sets block {@link #toolbarView} max-width, based on
+	 * editable width plus distance between furthest edge of the {@link #buttonView} and the editable.
+	 *
+	 * @private
+	 * @returns {String} maxWidth A maximum width that toolbar can have, sets in pixels.
+	 */
+	_getToolbarMaxWidth() {
+		const editableElement = this.editor.ui.view.editable.element;
+		const editableRect = new Rect( editableElement );
+		const buttonRect = new Rect( this.buttonView.element );
+		const isRTL = this.editor.locale.uiLanguageDirection === 'rtl';
+		const offset = isRTL ? ( buttonRect.left - editableRect.right ) + buttonRect.width : editableRect.left - buttonRect.left;
+
+		return toPx( editableRect.width + offset );
+	}
 }
 }
 
 
 /**
 /**

+ 80 - 0
packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js

@@ -19,13 +19,18 @@ import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbutton
 import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
 import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
 import Image from '@ckeditor/ckeditor5-image/src/image';
 import Image from '@ckeditor/ckeditor5-image/src/image';
 import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
 import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
 
 
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 
 
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+
 describe( 'BlockToolbar', () => {
 describe( 'BlockToolbar', () => {
 	let editor, element, blockToolbar;
 	let editor, element, blockToolbar;
+	let resizeCallback;
 
 
 	testUtils.createSinonSandbox();
 	testUtils.createSinonSandbox();
 
 
@@ -33,6 +38,20 @@ describe( 'BlockToolbar', () => {
 		element = document.createElement( 'div' );
 		element = document.createElement( 'div' );
 		document.body.appendChild( element );
 		document.body.appendChild( element );
 
 
+		// 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()
+			};
+		} );
+
 		return ClassicTestEditor.create( element, {
 		return ClassicTestEditor.create( element, {
 			plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, BlockQuote, Image, ImageCaption ],
 			plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, BlockQuote, Image, ImageCaption ],
 			blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'blockQuote' ]
 			blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'blockQuote' ]
@@ -64,6 +83,43 @@ describe( 'BlockToolbar', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	it( 'should accept the extended format of the toolbar config', () => {
+		return ClassicTestEditor
+			.create( element, {
+				plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, BlockQuote ],
+				blockToolbar: {
+					items: [ 'paragraph', 'heading1', 'heading2', 'blockQuote' ]
+				}
+			} )
+			.then( editor => {
+				blockToolbar = editor.plugins.get( BlockToolbar );
+
+				expect( blockToolbar.toolbarView.items ).to.length( 4 );
+
+				element.remove();
+
+				return editor.destroy();
+			} );
+	} );
+
+	it( 'should not group items when the config.shouldNotGroupWhenFull option is enabled', () => {
+		return ClassicTestEditor.create( element, {
+			plugins: [ BlockToolbar, Heading, HeadingButtonsUI, Paragraph, ParagraphButtonUI, BlockQuote ],
+			blockToolbar: {
+				items: [ 'paragraph', 'heading1', 'heading2', 'blockQuote' ],
+				shouldNotGroupWhenFull: true
+			}
+		} ).then( editor => {
+			const blockToolbar = editor.plugins.get( BlockToolbar );
+
+			expect( blockToolbar.toolbarView.options.shouldGroupWhenFull ).to.be.false;
+
+			element.remove();
+
+			return editor.destroy();
+		} );
+	} );
+
 	describe( 'child views', () => {
 	describe( 'child views', () => {
 		describe( 'panelView', () => {
 		describe( 'panelView', () => {
 			it( 'should create a view instance', () => {
 			it( 'should create a view instance', () => {
@@ -158,6 +214,30 @@ describe( 'BlockToolbar', () => {
 
 
 				expect( blockToolbar.panelView.isVisible ).to.be.false;
 				expect( blockToolbar.panelView.isVisible ).to.be.false;
 			} );
 			} );
+
+			it( 'should set a proper toolbar max-width', () => {
+				const viewElement = editor.ui.view.editable.element;
+
+				viewElement.style.width = '400px';
+
+				resizeCallback( [ {
+					target: viewElement,
+					contentRect: new Rect( viewElement )
+				} ] );
+
+				// The expected width should be a sum of the editable width and distance between
+				// block toolbar button and editable.
+				//           ---------------------------
+				//           |                         |
+				//   ___     |                         |
+				//  |__|     |        EDITABLE         |
+				//  button   |                         |
+				//           |                         |
+				//  <--------------max-width------------>
+				const expectedWidth = blockToolbar._getToolbarMaxWidth();
+
+				expect( blockToolbar.toolbarView.maxWidth ).to.be.equal( expectedWidth );
+			} );
 		} );
 		} );
 
 
 		describe( 'buttonView', () => {
 		describe( 'buttonView', () => {