Преглед изворни кода

Stabilized the toolbar grouping PoC.

Aleksander Nowodzinski пре 6 година
родитељ
комит
4f1d0ece33

+ 87 - 42
packages/ckeditor5-ui/src/toolbar/toolbarview.js

@@ -68,7 +68,7 @@ export default class ToolbarView extends View {
 		 * that would normally be wrapped to the next line.
 		 * that would normally be wrapped to the next line.
 		 *
 		 *
 		 * **Note:** It is created on demand when the space in the toolbar is scarce and only
 		 * **Note:** It is created on demand when the space in the toolbar is scarce and only
-		 * if {@link #enableWrappedItemsGroupping} has been called for this dropdown.
+		 * if {@link #enableWrappedItemsGrouping} has been called for this dropdown.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
 		 * @member {module:ui/dropdown/dropdownview~DropdownView} #wrappedItemsDropdown
 		 * @member {module:ui/dropdown/dropdownview~DropdownView} #wrappedItemsDropdown
@@ -91,6 +91,14 @@ export default class ToolbarView extends View {
 		 */
 		 */
 		this.set( 'class' );
 		this.set( 'class' );
 
 
+		/**
+		 * TODO
+		 *
+		 * @observable
+		 * @member {Boolean} #isGrouping
+		 */
+		this.set( 'isGrouping', false );
+
 		/**
 		/**
 		 * Helps cycling over focusable {@link #items} in the toolbar.
 		 * Helps cycling over focusable {@link #items} in the toolbar.
 		 *
 		 *
@@ -111,22 +119,7 @@ export default class ToolbarView extends View {
 			}
 			}
 		} );
 		} );
 
 
-		/**
-		 * A map that connects views belonging to {@link #items} with their Rects.
-		 *
-		 * It makes sense only when {@link #enableWrappedItemsGroupping} has been used.
-		 * When a toolbar item lands in the {@link #wrappedItemsDropdown}, it saves the item's
-		 * DOM rect so the algorithm can use it later on to decide if that particular item
-		 * can be "ungroupped" from the dropdown when there's enough space in the toolbar.
-		 *
-		 * Because "groupped" items in the dropdown are invisible, their Rects cannot be obtained, so
-		 * decision about their location is made using the Rect cached in this map beforehand.
-		 *
-		 * @readonly
-		 * @private
-		 * @member {Map.<module:ui/view~View,module:utils/dom/rect~Rect>}
-		 */
-		this._grouppedItemRects = new Map();
+		this._itemsWrappingLock = false;
 
 
 		this.setTemplate( {
 		this.setTemplate( {
 			tag: 'div',
 			tag: 'div',
@@ -159,16 +152,34 @@ export default class ToolbarView extends View {
 			this.focusTracker.add( item.element );
 			this.focusTracker.add( item.element );
 		}
 		}
 
 
-		this.items.on( 'add', ( evt, item ) => {
+		this.items.on( 'add', ( evt, item, index ) => {
 			this.focusTracker.add( item.element );
 			this.focusTracker.add( item.element );
+
+			if ( item !== this.wrappedItemsDropdown ) {
+				const wrappedItemsDropdownIndex = this.items.getIndex( this.wrappedItemsDropdown );
+
+				// Post–fixing if a new item was added after the wrapped items dropdown (pushed into #items).
+				// The dropdown must always be the last one no matter what.
+				if ( wrappedItemsDropdownIndex > -1 && index > wrappedItemsDropdownIndex ) {
+					this.items.add( this.items.remove( this.wrappedItemsDropdown ) );
+				}
+
+				this._groupOrUngroupWrappingItems( true );
+			}
 		} );
 		} );
 
 
 		this.items.on( 'remove', ( evt, item ) => {
 		this.items.on( 'remove', ( evt, item ) => {
 			this.focusTracker.remove( item.element );
 			this.focusTracker.remove( item.element );
+
+			if ( item !== this.wrappedItemsDropdown ) {
+				this._groupOrUngroupWrappingItems();
+			}
 		} );
 		} );
 
 
 		// Start listening for the keystrokes coming from #element.
 		// Start listening for the keystrokes coming from #element.
 		this.keystrokes.listenTo( this.element );
 		this.keystrokes.listenTo( this.element );
+
+		this._enableWrappedItemsGroupingOnResize();
 	}
 	}
 
 
 	/**
 	/**
@@ -206,11 +217,14 @@ export default class ToolbarView extends View {
 	 * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
 	 * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
 	 */
 	 */
 	fillFromConfig( config, factory ) {
 	fillFromConfig( config, factory ) {
-		config.map( name => {
+		// The toolbar is filled in in the reverse order for the toolbar grouping to work properly.
+		// If we filled it in in the natural order, items that wrap to the next line would be grouped
+		// in a revere order.
+		config.reverse().map( name => {
 			if ( name == '|' ) {
 			if ( name == '|' ) {
-				this.items.add( new ToolbarSeparatorView() );
+				this.items.add( new ToolbarSeparatorView(), 0 );
 			} else if ( factory.has( name ) ) {
 			} else if ( factory.has( name ) ) {
-				this.items.add( factory.create( name ) );
+				this.items.add( factory.create( name ), 0 );
 			} else {
 			} else {
 				/**
 				/**
 				 * There was a problem processing the configuration of the toolbar. The item with the given
 				 * There was a problem processing the configuration of the toolbar. The item with the given
@@ -250,22 +264,22 @@ export default class ToolbarView extends View {
 	 * not `display: none`) will cause lots of warnings in the console from the utilities analyzing
 	 * not `display: none`) will cause lots of warnings in the console from the utilities analyzing
 	 * 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() {
+	_enableWrappedItemsGroupingOnResize() {
 		let oldRect;
 		let oldRect;
 
 
-		this._checkItemsWrappingAndUnwrapping();
+		this._groupOrUngroupWrappingItems();
 
 
 		// TODO: stopObserving on destroy();
 		// TODO: stopObserving on destroy();
 		new RectObserver( this.element ).observe( newRect => {
 		new RectObserver( this.element ).observe( newRect => {
 			if ( oldRect && oldRect.width !== newRect.width ) {
 			if ( oldRect && oldRect.width !== newRect.width ) {
-				this._checkItemsWrappingAndUnwrapping();
+				this._groupOrUngroupWrappingItems();
 			}
 			}
 
 
 			oldRect = newRect;
 			oldRect = newRect;
 		} );
 		} );
 	}
 	}
 
 
-	get _grouppedItems() {
+	get _groupedItems() {
 		return this.wrappedItemsDropdown.toolbarView.items;
 		return this.wrappedItemsDropdown.toolbarView.items;
 	}
 	}
 
 
@@ -282,7 +296,7 @@ export default class ToolbarView extends View {
 		const locale = this.locale;
 		const locale = this.locale;
 
 
 		this.wrappedItemsDropdown = createDropdown( locale );
 		this.wrappedItemsDropdown = createDropdown( locale );
-		this.wrappedItemsDropdown.class = 'ck-toolbar__groupped-dropdown';
+		this.wrappedItemsDropdown.class = 'ck-toolbar__grouped-dropdown';
 		addToolbarToDropdown( this.wrappedItemsDropdown, [] );
 		addToolbarToDropdown( this.wrappedItemsDropdown, [] );
 
 
 		this.wrappedItemsDropdown.buttonView.set( {
 		this.wrappedItemsDropdown.buttonView.set( {
@@ -292,8 +306,12 @@ export default class ToolbarView extends View {
 		} );
 		} );
 	}
 	}
 
 
+	get _hasWrappedItemsDropdown() {
+		return this.wrappedItemsDropdown && this.items.has( this.wrappedItemsDropdown );
+	}
+
 	/**
 	/**
-	 * When called it will remove the last {@link #_lastNonGrouppedItem regular item} from {@link #items}
+	 * When called it will remove the last {@link #_lastNonGroupedItem 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
@@ -306,19 +324,19 @@ export default class ToolbarView extends View {
 			this._createWrappedItemsDropdown();
 			this._createWrappedItemsDropdown();
 		}
 		}
 
 
-		if ( !this.items.has( this.wrappedItemsDropdown ) ) {
+		if ( !this._hasWrappedItemsDropdown ) {
 			this.items.add( this.wrappedItemsDropdown );
 			this.items.add( this.wrappedItemsDropdown );
 		}
 		}
 
 
-		let lastNonGrouppedItem;
+		let lastNonGroupedItem;
 
 
-		if ( this.items.has( this.wrappedItemsDropdown ) ) {
-			lastNonGrouppedItem = this.items.length > 1 ? this.items.get( this.items.length - 2 ) : null;
+		if ( this._hasWrappedItemsDropdown ) {
+			lastNonGroupedItem = this.items.length > 1 ? this.items.get( this.items.length - 2 ) : null;
 		} else {
 		} else {
-			lastNonGrouppedItem = this.items.last;
+			lastNonGroupedItem = this.items.last;
 		}
 		}
 
 
-		this._grouppedItems.add( this.items.remove( lastNonGrouppedItem ), 0 );
+		this._groupedItems.add( this.items.remove( lastNonGroupedItem ), 0 );
 	}
 	}
 
 
 	/**
 	/**
@@ -330,9 +348,9 @@ export default class ToolbarView extends View {
 	 * @protected
 	 * @protected
 	 */
 	 */
 	_ungroupFirstItem() {
 	_ungroupFirstItem() {
-		this.items.add( this._grouppedItems.remove( 0 ), this.items.length - 1 );
+		this.items.add( this._groupedItems.remove( 0 ), this.items.length - 1 );
 
 
-		if ( !this._grouppedItems.length ) {
+		if ( !this._groupedItems.length ) {
 			this.items.remove( this.wrappedItemsDropdown );
 			this.items.remove( this.wrappedItemsDropdown );
 		}
 		}
 	}
 	}
@@ -368,31 +386,58 @@ export default class ToolbarView extends View {
 	 *
 	 *
 	 * @protected
 	 * @protected
 	 */
 	 */
-	_checkItemsWrappingAndUnwrapping() {
+	_groupOrUngroupWrappingItems() {
+		// Do not check when another check is going to avoid infinite loops.
+		// This method is called upon adding and removing #items and it adds and removes
+		// #items itself, so that would be a disaster.
+		if ( this._itemsWrappingLock ) {
+			return;
+		}
+
+		// There's no way to check wrapping when there is no element (before #render()).
+		// Or when element has no parent because ClientRects won't work when #element not in DOM.
 		if ( !this.element || !this.element.parentNode ) {
 		if ( !this.element || !this.element.parentNode ) {
 			return;
 			return;
 		}
 		}
 
 
-		let wereItemsGroupped;
+		this._itemsWrappingLock = true;
+
+		let wereItemsGrouped;
 
 
+		// Group #items as long as any wraps to the next line. This will happen, for instance,
+		// when the toolbar is getting narrower and there's less and less space in it.
 		while ( this._areItemsWrapping ) {
 		while ( this._areItemsWrapping ) {
 			this._groupLastItem();
 			this._groupLastItem();
-			wereItemsGroupped = true;
+
+			wereItemsGrouped = true;
 		}
 		}
 
 
-		if ( !wereItemsGroupped && this.wrappedItemsDropdown && this._grouppedItems.length ) {
-			while ( !this._areItemsWrapping ) {
+		// If none were grouped now but there were some items already grouped before,
+		// then maybe let's see if some of them can be ungrouped. This happens when,
+		// for instance, the toolbar is stretching and there's more space in it than before.
+		if ( !wereItemsGrouped && this._hasWrappedItemsDropdown ) {
+			let areItemsWraping;
+
+			// Ungroup items as long as none are wrapping to the next line...
+			while ( !( areItemsWraping = this._areItemsWrapping ) ) {
 				this._ungroupFirstItem();
 				this._ungroupFirstItem();
 
 
-				if ( !this._grouppedItems.length ) {
+				// ...or there are none to ungroup left.
+				if ( !this._groupedItems.length ) {
 					break;
 					break;
 				}
 				}
 			}
 			}
 
 
-			if ( this._areItemsWrapping ) {
+			// If the ungrouping ended up with some item wrapping to the next line,
+			// put it back to the group toolbar (undo the last ungroup). We don't know whether
+			// an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
+			// clean–up is vital.
+			if ( areItemsWraping ) {
 				this._groupLastItem();
 				this._groupLastItem();
 			}
 			}
 		}
 		}
+
+		this._itemsWrappingLock = false;
 	}
 	}
 }
 }
 
 

+ 2 - 6
packages/ckeditor5-ui/theme/components/toolbar/toolbar.css

@@ -5,10 +5,6 @@
 
 
 @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;
 
 
@@ -34,8 +30,8 @@
 	width: 100%;
 	width: 100%;
 }
 }
 
 
-/* Pull the groupped items dropdown to the right */
-.ck.ck-toolbar__groupped-dropdown {
+/* Pull the grouped items dropdown to the right */
+.ck.ck-toolbar__grouped-dropdown {
 	margin-left: auto;
 	margin-left: auto;
 
 
 	/*
 	/*