8
0
Aleksander Nowodzinski 6 лет назад
Родитель
Сommit
8d738c7501

+ 67 - 0
packages/ckeditor5-ui/src/rectobserver.js

@@ -0,0 +1,67 @@
+/**
+ * @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/RectObserver
+ */
+
+/* globals setTimeout, clearTimeout, ResizeObserver */
+
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
+
+export default class RectObserver {
+	constructor( element ) {
+		this.element = element;
+	}
+
+	observe( callback ) {
+		if ( typeof ResizeObserver === 'function' ) {
+			this._observer = new ResizeObserver( entries => {
+				callback( new Rect( entries[ 0 ].contentRect ) );
+			} ).observe( this.element );
+		} else {
+			let previousRect;
+
+			const hasRectChanged = () => {
+				const currentRect = new Rect( this.element );
+				const hasChanged = previousRect && !previousRect.isEqual( currentRect );
+
+				previousRect = currentRect;
+
+				return hasChanged;
+			};
+
+			const periodicCheck = () => {
+				if ( hasRectChanged() ) {
+					callback( previousRect );
+				}
+
+				this._checkTimeout = setTimeout( periodicCheck, 500 );
+			};
+
+			this.listenTo( global.window, 'resize', () => {
+				if ( hasRectChanged() ) {
+					callback( previousRect );
+				}
+			} );
+
+			periodicCheck();
+		}
+	}
+
+	stopObserving() {
+		if ( this._observer ) {
+			this._observer.disconnect();
+		} else {
+			this.stopListening();
+			clearTimeout( this._checkTimeout );
+		}
+	}
+}
+
+mix( RectObserver, DomEmitterMixin );

+ 44 - 99
packages/ckeditor5-ui/src/toolbar/toolbarview.js

@@ -14,21 +14,15 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
 import FocusCycler from '../focuscycler';
 import FocusCycler from '../focuscycler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
 import ToolbarSeparatorView from './toolbarseparatorview';
 import ToolbarSeparatorView from './toolbarseparatorview';
+import RectObserver from '../rectobserver';
 import preventDefault from '../bindings/preventdefault.js';
 import preventDefault from '../bindings/preventdefault.js';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
-import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import { createDropdown, addToolbarToDropdown } from '../dropdown/utils';
 import { createDropdown, addToolbarToDropdown } from '../dropdown/utils';
 import { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import verticalDotsIcon from '@ckeditor/ckeditor5-core/theme/icons/three-vertical-dots.svg';
 import verticalDotsIcon from '@ckeditor/ckeditor5-core/theme/icons/three-vertical-dots.svg';
 
 
 import '../../theme/components/toolbar/toolbar.css';
 import '../../theme/components/toolbar/toolbar.css';
 
 
-// This is the offset for the enableWrappedItemsGroupping() method. It estimates the width of the
-// scrollbar. There's no way to tell when the vertical page scrollbar appears using the DOM API so
-// when wrapping toolbar items to the next line we must consider it may show up at any time
-// (e.g. user wrote more content). This is the h–distance the scrollbar will consume when it appears.
-const SUDDEN_SCROLL_SAFETY_OFFSET = 25;
-
 /**
 /**
  * The toolbar view class.
  * The toolbar view class.
  *
  *
@@ -257,49 +251,29 @@ export default class ToolbarView extends View {
 	 * the geometry of the toolbar items — they depend on the toolbar to be visible in DOM.
 	 * the geometry of the toolbar items — they depend on the toolbar to be visible in DOM.
 	 */
 	 */
 	enableWrappedItemsGroupping() {
 	enableWrappedItemsGroupping() {
-		this._checkItemsWrappingAndUnwrapping();
+		let oldRect;
 
 
-		this.listenTo( global.window, 'resize', () => {
-			this._checkItemsWrappingAndUnwrapping();
-		} );
-	}
+		this._checkItemsWrappingAndUnwrapping();
 
 
-	/**
-	 * Returns the last of {@link #items} which is not {@link #wrappedItemsDropdown}.
-	 *
-	 * @protected
-	 */
-	get _lastRegularItem() {
-		if ( this._isWrappedItemsDropdownInItems ) {
-			if ( this.items.length > 1 ) {
-				return this.items.get( this.items.length - 2 );
-			} else {
-				return null;
+		// TODO: stopObserving on destroy();
+		new RectObserver( this.element ).observe( newRect => {
+			if ( oldRect && oldRect.width !== newRect.width ) {
+				this._checkItemsWrappingAndUnwrapping();
 			}
 			}
-		} else {
-			if ( this.items.length ) {
-				return this.items.last;
-			} else {
-				return null;
-			}
-		}
+
+			oldRect = newRect;
+		} );
 	}
 	}
 
 
-	/**
-	 * Returns `true` when {@link #wrappedItemsDropdown} exists and currently is in {@link #items}.
-	 * `false` otherwise.
-	 *
-	 * @protected
-	 */
-	get _isWrappedItemsDropdownInItems() {
-		return this.wrappedItemsDropdown && this.items.has( this.wrappedItemsDropdown );
+	get _grouppedItems() {
+		return this.wrappedItemsDropdown.toolbarView.items;
 	}
 	}
 
 
 	/**
 	/**
 	 * Creates the {@link #wrappedItemsDropdown} on demand. Used when the space in the toolbar
 	 * Creates the {@link #wrappedItemsDropdown} on demand. Used when the space in the toolbar
 	 * is scarce and some items start wrapping and need grouping.
 	 * is scarce and some items start wrapping and need grouping.
 	 *
 	 *
-	 * See {@link #_groupLastRegularItem}.
+	 * See {@link #_groupLastItem}.
 	 *
 	 *
 	 * @protected
 	 * @protected
 	 */
 	 */
@@ -319,7 +293,7 @@ export default class ToolbarView extends View {
 	}
 	}
 
 
 	/**
 	/**
-	 * When called it will remove the last {@link #_lastRegularItem regular item} from {@link #items}
+	 * When called it will remove the last {@link #_lastNonGrouppedItem regular item} from {@link #items}
 	 * and move it to the {@link #wrappedItemsDropdown}.
 	 * and move it to the {@link #wrappedItemsDropdown}.
 	 *
 	 *
 	 * If the dropdown does not exist or does not
 	 * If the dropdown does not exist or does not
@@ -327,66 +301,39 @@ export default class ToolbarView extends View {
 	 *
 	 *
 	 * @protected
 	 * @protected
 	 */
 	 */
-	_groupLastRegularItem() {
-		// Add the groupped list dropdown if not already there.
-		if ( !this._isWrappedItemsDropdownInItems ) {
-			if ( !this.wrappedItemsDropdown ) {
-				this._createWrappedItemsDropdown();
-			}
+	_groupLastItem() {
+		if ( !this.wrappedItemsDropdown ) {
+			this._createWrappedItemsDropdown();
+		}
 
 
+		if ( !this.items.has( this.wrappedItemsDropdown ) ) {
 			this.items.add( this.wrappedItemsDropdown );
 			this.items.add( this.wrappedItemsDropdown );
 		}
 		}
 
 
-		const lastItem = this._lastRegularItem;
+		let lastNonGrouppedItem;
 
 
-		this._grouppedItemRects.set( lastItem, new Rect( lastItem.element ) );
+		if ( this.items.has( this.wrappedItemsDropdown ) ) {
+			lastNonGrouppedItem = this.items.length > 1 ? this.items.get( this.items.length - 2 ) : null;
+		} else {
+			lastNonGrouppedItem = this.items.last;
+		}
 
 
-		this.wrappedItemsDropdown.toolbarView.items.add( this.items.remove( lastItem ), 0 );
+		this._grouppedItems.add( this.items.remove( lastNonGrouppedItem ), 0 );
 	}
 	}
 
 
 	/**
 	/**
 	 * Moves the very first item from the toolbar belonging to {@link #wrappedItemsDropdown} back
 	 * Moves the very first item from the toolbar belonging to {@link #wrappedItemsDropdown} back
 	 * to the {@link #items} collection.
 	 * to the {@link #items} collection.
 	 *
 	 *
-	 * In some way, it's the opposite of {@link #_groupLastRegularItem}.
-	 *
-	 * @protected
-	 */
-	_ungroupFirstGrouppedItem() {
-		this.items.add( this.wrappedItemsDropdown.toolbarView.items.remove( 0 ), this.items.length - 1 );
-	}
-
-	/**
-	 * When called it will try to moves the very first item from the toolbar belonging to {@link #wrappedItemsDropdown}
-	 * back to the {@link #items} collection.
-	 *
-	 * Whether the items is moved or not, it depends on the remaining space in the toolbar, which is
-	 * verified using {@link #_grouppedItemRects}.
+	 * In some way, it's the opposite of {@link #_groupLastItem}.
 	 *
 	 *
 	 * @protected
 	 * @protected
 	 */
 	 */
-	_tryUngroupLastItem() {
-		const firstGrouppedItem = this.wrappedItemsDropdown.toolbarView.items.get( 0 );
-		const firstGrouppedItemRect = this._grouppedItemRects.get( firstGrouppedItem );
-		const wrappedItemsDropdownRect = new Rect( this.wrappedItemsDropdown.element );
-		const lastRegularItem = this._lastRegularItem;
-		let leftBoundary;
-
-		if ( lastRegularItem ) {
-			leftBoundary = new Rect( lastRegularItem.element ).right;
-		} else {
-			leftBoundary = new Rect( this.element ).left;
-		}
+	_ungroupFirstItem() {
+		this.items.add( this._grouppedItems.remove( 0 ), this.items.length - 1 );
 
 
-		// If there's only one grouped item, then when ungrouped, it should replace the wrapped items
-		// dropdown. Consider that fact when analyzing rects, because the conditions are different.
-		if ( this.wrappedItemsDropdown.toolbarView.items.length === 1 ) {
-			if ( leftBoundary + firstGrouppedItemRect.width + SUDDEN_SCROLL_SAFETY_OFFSET < wrappedItemsDropdownRect.right ) {
-				this._ungroupFirstGrouppedItem();
-				this.items.remove( this.wrappedItemsDropdown );
-			}
-		} else if ( leftBoundary + firstGrouppedItemRect.width + SUDDEN_SCROLL_SAFETY_OFFSET < wrappedItemsDropdownRect.left ) {
-			this._ungroupFirstGrouppedItem();
+		if ( !this._grouppedItems.length ) {
+			this.items.remove( this.wrappedItemsDropdown );
 		}
 		}
 	}
 	}
 
 
@@ -401,12 +348,11 @@ export default class ToolbarView extends View {
 			return false;
 			return false;
 		}
 		}
 
 
-		const firstItem = this.items.first;
-
 		if ( this.items.length === 1 ) {
 		if ( this.items.length === 1 ) {
 			return false;
 			return false;
 		}
 		}
 
 
+		const firstItem = this.items.first;
 		const firstItemRect = new Rect( firstItem.element );
 		const firstItemRect = new Rect( firstItem.element );
 		const lastItemRect = new Rect( this.items.last.element );
 		const lastItemRect = new Rect( this.items.last.element );
 
 
@@ -427,26 +373,25 @@ export default class ToolbarView extends View {
 			return;
 			return;
 		}
 		}
 
 
+		let wereItemsGroupped;
+
 		while ( this._areItemsWrapping ) {
 		while ( this._areItemsWrapping ) {
-			this._groupLastRegularItem();
+			this._groupLastItem();
+			wereItemsGroupped = true;
 		}
 		}
 
 
-		if ( this._isWrappedItemsDropdownInItems ) {
-			// Post-fixing just in case the page content grows up and a scrollbar appears.
-			// If the last item is too close to the wrapped items dropdown, put it in the
-			// dropdown too: if scrollbar shows up, it could push the dropdown to the next line.
-			const wrappedItemsDropdownRect = new Rect( this.wrappedItemsDropdown.element );
-			const lastRegularItem = this._lastRegularItem;
+		if ( !wereItemsGroupped && this.wrappedItemsDropdown && this._grouppedItems.length ) {
+			while ( !this._areItemsWrapping ) {
+				this._ungroupFirstItem();
 
 
-			if ( lastRegularItem ) {
-				const lastRegularItemRect = new Rect( lastRegularItem.element );
-
-				if ( lastRegularItemRect.right + SUDDEN_SCROLL_SAFETY_OFFSET > wrappedItemsDropdownRect.left ) {
-					this._groupLastRegularItem();
+				if ( !this._grouppedItems.length ) {
+					break;
 				}
 				}
 			}
 			}
 
 
-			this._tryUngroupLastItem();
+			if ( this._areItemsWrapping ) {
+				this._groupLastItem();
+			}
 		}
 		}
 	}
 	}
 }
 }

+ 4 - 0
packages/ckeditor5-ui/theme/components/toolbar/toolbar.css

@@ -5,6 +5,10 @@
 
 
 @import "../../mixins/_unselectable.css";
 @import "../../mixins/_unselectable.css";
 
 
+:root {
+	--ck-toolbar-groupped-dropdown-arrow-size: calc(0.5 * var(--ck-icon-size));
+}
+
 .ck.ck-toolbar {
 .ck.ck-toolbar {
 	@mixin ck-unselectable;
 	@mixin ck-unselectable;