Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #554 from ckeditor/i/6449
Browse files Browse the repository at this point in the history
Feature: The `BlockToolbar` should group items when there is no place to show them all. Closes ckeditor/ckeditor5#6449. Closes ckeditor/ckeditor5#6575. Closes ckeditor/ckeditor5#6570.

Bulletproofed the `ToolbarView#maxWidth` and items grouping when the toolbar is invisible.
  • Loading branch information
oleq committed Apr 9, 2020
2 parents 3c25da5 + b9fedee commit fcd9c4e
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 9 deletions.
28 changes: 27 additions & 1 deletion docs/features/blocktoolbar.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ To add this feature to your editor install the [`@ckeditor/ckeditor5-ui`](https:
npm install --save @ckeditor/ckeditor5-ui
```

And add it to your plugin list:
Add the `BlockToolbar` to your plugin list and configure the feature using the `blockToolbar` property:

```js
import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar';
Expand All @@ -74,6 +74,32 @@ BalloonEditor
.catch( ... );
```

You can also use the `shouldNotGroupWhenFull` option to prevent {@link module:core/editor/editorconfig~EditorConfig#toolbar automatic items grouping} in the block toolbar:

```js
import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar';
import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui';
import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui';

BalloonEditor
.create( document.querySelector( '#editor' ), {
plugins: [ BlockToolbar, ParagraphButtonUI, HeadingButtonsUI, ... ],
blockToolbar: {
items: [
'paragraph', 'heading1', 'heading2', 'heading3',
'|',
'bulletedList', 'numberedList',
'|',
'blockQuote', 'imageUpload'
],
shouldNotGroupWhenFull: true
},
toolbar: [ ... ]
} )
.then( ... )
.catch( ... );
```

<info-box info>
Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
</info-box>
Expand Down
4 changes: 2 additions & 2 deletions src/toolbar/balloon/balloontoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,9 @@ function getBalloonPositions( isBackward ) {
*
* const config = {
* balloonToolbar: {
* items: [ 'bold', 'italic', 'undo', 'redo' ]
* items: [ 'bold', 'italic', 'undo', 'redo' ],
* shouldNotGroupWhenFull: true
* },
* shouldNotGroupWhenFull: true
* };
*
* @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#balloonToolbar
Expand Down
102 changes: 99 additions & 3 deletions src/toolbar/block/blocktoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ import clickOutsideHandler from '../../bindings/clickoutsidehandler';

import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
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';

const toPx = toUnit( 'px' );

/**
* The block toolbar plugin.
*
Expand Down Expand Up @@ -77,6 +83,14 @@ export default class BlockToolbar extends Plugin {
constructor( 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.
*
Expand All @@ -98,6 +112,20 @@ export default class BlockToolbar extends Plugin {
*/
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.
clickOutsideHandler( {
emitter: this.panelView,
Expand Down Expand Up @@ -149,14 +177,25 @@ export default class BlockToolbar extends Plugin {
*/
afterInit() {
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.
for ( const item of this.toolbarView.items ) {
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();
} );
} );
}
}

/**
Expand All @@ -178,7 +217,10 @@ export default class BlockToolbar extends Plugin {
* @returns {module:ui/toolbar/toolbarview~ToolbarView}
*/
_createToolbarView() {
const toolbarView = new ToolbarView( this.editor.locale );
const shouldGroupWhenFull = !this._blockToolbarConfig.shouldNotGroupWhenFull;
const toolbarView = new ToolbarView( this.editor.locale, {
shouldGroupWhenFull
} );

toolbarView.extendTemplate( {
attributes: {
Expand Down Expand Up @@ -325,6 +367,32 @@ export default class BlockToolbar extends Plugin {
_showPanel() {
const wasVisible = this.panelView.isVisible;

// So here's the thing: If there was no initial panelView#show() or these two were in different order, the toolbar
// positioning will break in RTL editors. Weird, right? What you show know is that the toolbar
// grouping works thanks to:
//
// * the ResizeObserver, which kicks in as soon as the toolbar shows up in DOM (becomes visible again).
// * the observable ToolbarView#maxWidth, which triggers re-grouping when changed.
//
// Here are the possible scenarios:
//
// 1. (WRONG ❌) If the #maxWidth is set when the toolbar is invisible, it won't affect item grouping (no DOMRects, no grouping).
// Then, when panelView.pin() is called, the position of the toolbar will be calculated for the old
// items grouping state, and when finally ResizeObserver kicks in (hey, the toolbar is visible now, right?)
// it will group/ungroup some items and the length of the toolbar will change. But since in RTL the toolbar
// is attached on the right side and the positioning uses CSS "left", it will result in the toolbar shifting
// to the left and being displayed in the wrong place.
// 2. (WRONG ❌) If the panelView.pin() is called first and #maxWidth set next, then basically the story repeats. The balloon
// calculates the position for the old toolbar grouping state, then the toolbar re-groups items and because
// it is positioned using CSS "left" it will move.
// 3. (RIGHT ✅) We show the panel first (the toolbar does re-grouping but it does not matter), then the #maxWidth
// is set allowing the toolbar to re-group again and finally panelView.pin() does the positioning when the
// items grouping state is stable and final.
//
// https://github.com/ckeditor/ckeditor5/issues/6449, https://github.com/ckeditor/ckeditor5/issues/6575
this.panelView.show();
this.toolbarView.maxWidth = this._getToolbarMaxWidth();

this.panelView.pin( {
target: this.buttonView.element,
limiter: this.editor.ui.getEditableElement()
Expand Down Expand Up @@ -388,6 +456,23 @@ export default class BlockToolbar extends Plugin {
this.buttonView.top = position.top;
this.buttonView.left = position.left;
}

/**
* Gets the {@link #toolbarView} max-width, based on
* editable width plus distance between farthest edge of the {@link #buttonView} and the editable.
*
* @private
* @returns {String} maxWidth A maximum width that toolbar can have, 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 );
}
}

/**
Expand All @@ -404,6 +489,17 @@ export default class BlockToolbar extends Plugin {
* blockToolbar: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ]
* };
*
* ## Configuring items grouping
*
* You can prevent automatic items grouping by setting the `shouldNotGroupWhenFull` option:
*
* const config = {
* blockToolbar: {
* items: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ],
* shouldNotGroupWhenFull: true
* },
* };
*
* Read more about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
*
* @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#blockToolbar
Expand Down
27 changes: 25 additions & 2 deletions src/toolbar/toolbarview.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,15 @@ class DynamicGrouping {
*/
this.cachedPadding = null;

/**
* A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
* and should be executed immediately the next time the toolbar shows up.
*
* @readonly
* @member {Boolean}
*/
this.shouldUpdateGroupingOnNextResize = false;

// Only those items that were not grouped are visible to the user.
view.itemsView.children.bindTo( this.ungroupedItems ).using( item => item );

Expand Down Expand Up @@ -614,11 +623,23 @@ class DynamicGrouping {
// Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
// for instance before #render(), or after render but without a parent or a parent detached
// from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
// nothing else.
// nothing else. This happens, for instance, when the toolbar is detached from DOM and
// some logic adds or removes its #items.
if ( !this.viewElement.ownerDocument.body.contains( this.viewElement ) ) {
return;
}

// Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
// and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
// the toolbar is visible (the next ResizeObserver callback execution). This is handy because
// the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
// time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
if ( !this.viewElement.offsetParent ) {
this.shouldUpdateGroupingOnNextResize = true;

return;
}

let wereItemsGrouped;

// Group #items as long as some wrap to the next row. This will happen, for instance,
Expand Down Expand Up @@ -701,7 +722,9 @@ class DynamicGrouping {

// TODO: Consider debounce.
this.resizeObserver = new ResizeObserver( this.viewElement, entry => {
if ( !previousWidth || previousWidth !== entry.contentRect.width ) {
if ( !previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize ) {
this.shouldUpdateGroupingOnNextResize = false;

this._updateGrouping();

previousWidth = entry.contentRect.width;
Expand Down
6 changes: 5 additions & 1 deletion tests/manual/blocktoolbar/blocktoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import BlockToolbar from '../../../src/toolbar/block/blocktoolbar';
BalloonEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, List, Paragraph, Heading, Image, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar ],
blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList' ]
blockToolbar: [
'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph',
'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', 'heading3',
'bulletedList', 'numberedList'
]
} )
.then( editor => {
window.editor = editor;
Expand Down
63 changes: 63 additions & 0 deletions tests/manual/blocktoolbar/rtl.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<button class="external-type">Start external typing</button>
<button class="external-delete">Start external deleting</button>

<div class="wrapper">
<div id="editor">
<h2>The three greatest things you learn from traveling</h2>
<p>
Like all the great things on earth traveling teaches us by example. Here are some of the most precious lessons
I’ve learned over the years of traveling.
</p>

<figure class="image">
<img src="./umbrellas.jpg" alt="Three Monks walking on ancient temple.">
<figcaption>Leaving your comfort zone might lead you to such beautiful sceneries like this one.</figcaption>
</figure>

<h3>Appreciation of diversity</h3>
<p>
Getting used to an entirely different culture can be challenging. While it’s also nice to learn about
cultures online or from books, nothing comes close to experiencing cultural diversity in person.
You learn to appreciate each and every single one of the differences while you become more culturally fluid.
</p>

<h3>Improvisation</h3>
<p>
Life doesn't allow us to execute every single plan perfectly. This especially seems to be the case when
you travel. You plan it down to every minute with a big checklist; but when it comes to executing it,
something always comes up and you’re left with your improvising skills. You learn to adapt as you go.
Here’s how my travel checklist looks now:
</p>

<ul>
<li>buy the ticket</li>
<li>start your adventure</li>
</ul>

<h3>Confidence</h3>
<p>
Going to a new place can be quite terrifying. While change and uncertainty makes us scared, traveling
teaches us how ridiculous it is to be afraid of something before it happens. The moment you face your
fear and see there was nothing to be afraid of, is the moment you discover bliss.
</p>
</div>
</div>

<style>
#editor {
margin: 0 auto;
max-width: 800px;
}

.wrapper {
padding: 50px 20px;
}

.ck-block-toolbar-button {
transform: translateX( -10px );
}

[dir="rtl"] .ck-block-toolbar-button {
transform: translateX( 10px );
}
</style>
34 changes: 34 additions & 0 deletions tests/manual/blocktoolbar/rtl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/

/* globals window, document, console:false, */

import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import List from '@ckeditor/ckeditor5-list/src/list';
import Image from '@ckeditor/ckeditor5-image/src/image';
import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import HeadingButtonsUI from '@ckeditor/ckeditor5-heading/src/headingbuttonsui';
import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui';
import BlockToolbar from '../../../src/toolbar/block/blocktoolbar';

BalloonEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Essentials, List, Paragraph, Heading, Image, ImageCaption, HeadingButtonsUI, ParagraphButtonUI, BlockToolbar ],
blockToolbar: [
'paragraph', 'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph',
'heading1', 'heading2', 'heading3', 'bulletedList', 'numberedList', 'paragraph', 'heading1', 'heading2', 'heading3',
'bulletedList', 'numberedList'
],
language: 'ar'
} )
.then( editor => {
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );
4 changes: 4 additions & 0 deletions tests/manual/blocktoolbar/rtl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Block toolbar in RTL editor

1. Check if the button appears on the right side of editable.
2. Check if the toolbar shows up attached correctly to the button.
Loading

0 comments on commit fcd9c4e

Please sign in to comment.