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 #536 from ckeditor/i/5597
Browse files Browse the repository at this point in the history
Feature: The `BalloonToolbar` plugin should group items when its width is close to related editable's width. Closes ckeditor/ckeditor5#5597. Closes ckeditor/ckeditor5#5501.

BREAKING CHANGE: The `BalloonToolbar` plugin groups overflowing items now by default. This can be disabled via the editor configuration by setting `config.balloonToolbar.shouldNotGroupWhenFull = true`.

[`BalloonPanelView.defaultPositions`](https://ckeditor.com/docs/ckeditor5/latest/api/module_ui_panel_balloon_balloonpanelview-BalloonPanelView.html#static-member-defaultPositions) has been extended with additional positions. Please refer to the documentation to learn more.
  • Loading branch information
oleq committed Feb 26, 2020
2 parents 334270b + 0395c4f commit d36fd23
Show file tree
Hide file tree
Showing 9 changed files with 644 additions and 124 deletions.
407 changes: 295 additions & 112 deletions src/panel/balloon/balloonpanelview.js

Large diffs are not rendered by default.

77 changes: 72 additions & 5 deletions src/toolbar/balloon/balloontoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
import normalizeToolbarConfig from '../normalizetoolbarconfig';
import { debounce } from 'lodash-es';
import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver';
import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';

const toPx = toUnit( 'px' );

/**
* The contextual toolbar.
Expand Down Expand Up @@ -44,6 +48,14 @@ export default class BalloonToolbar extends Plugin {
constructor( editor ) {
super( editor );

/**
* A cached and normalized `config.balloonToolbar` object.
*
* @type {module:core/editor/editorconfig~EditorConfig#balloonToolbar}
* @private
*/
this._balloonConfig = normalizeToolbarConfig( editor.config.get( 'balloonToolbar' ) );

/**
* The toolbar view displayed in the balloon.
*
Expand All @@ -66,6 +78,20 @@ export default class BalloonToolbar extends Plugin {
this.focusTracker.add( this.toolbarView.element );
} );

/**
* 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#balloonToolbar configuration}.
*
* **Note:** Created in {@link #init}.
*
* @protected
* @member {module:utils/dom/resizeobserver~ResizeObserver}
*/
this._resizeObserver = null;

/**
* The contextual balloon plugin instance.
*
Expand Down Expand Up @@ -125,6 +151,20 @@ export default class BalloonToolbar extends Plugin {
this.show();
}
} );

if ( !this._balloonConfig.shouldNotGroupWhenFull ) {
this.listenTo( editor, 'ready', () => {
const editableElement = editor.ui.view.editable.element;

// Set #toolbarView's max-width on the initialization and update it on the editable resize.
this._resizeObserver = new ResizeObserver( editableElement, () => {
// The max-width equals 90% of the editable's width for the best user experience.
// The value keeps the balloon very close to the boundaries of the editable and limits the cases
// when the balloon juts out from the editable element it belongs to.
this.toolbarView.maxWidth = toPx( new Rect( editableElement ).width * .9 );
} );
} );
}
}

/**
Expand All @@ -134,10 +174,9 @@ export default class BalloonToolbar extends Plugin {
* @inheritDoc
*/
afterInit() {
const config = normalizeToolbarConfig( this.editor.config.get( 'balloonToolbar' ) );
const factory = this.editor.ui.componentFactory;

this.toolbarView.fillFromConfig( config.items, factory );
this.toolbarView.fillFromConfig( this._balloonConfig.items, factory );
}

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

toolbarView.extendTemplate( {
attributes: {
Expand Down Expand Up @@ -260,6 +302,10 @@ export default class BalloonToolbar extends Plugin {
this._fireSelectionChangeDebounced.cancel();
this.toolbarView.destroy();
this.focusTracker.destroy();

if ( this._resizeObserver ) {
this._resizeObserver.destroy();
}
}

/**
Expand Down Expand Up @@ -289,23 +335,33 @@ function getBalloonPositions( isBackward ) {
defaultPositions.northWestArrowSouth,
defaultPositions.northWestArrowSouthWest,
defaultPositions.northWestArrowSouthEast,
defaultPositions.northWestArrowSouthMiddleEast,
defaultPositions.northWestArrowSouthMiddleWest,
defaultPositions.southWestArrowNorth,
defaultPositions.southWestArrowNorthWest,
defaultPositions.southWestArrowNorthEast
defaultPositions.southWestArrowNorthEast,
defaultPositions.southWestArrowNorthMiddleWest,
defaultPositions.southWestArrowNorthMiddleEast
] : [
defaultPositions.southEastArrowNorth,
defaultPositions.southEastArrowNorthEast,
defaultPositions.southEastArrowNorthWest,
defaultPositions.southEastArrowNorthMiddleEast,
defaultPositions.southEastArrowNorthMiddleWest,
defaultPositions.northEastArrowSouth,
defaultPositions.northEastArrowSouthEast,
defaultPositions.northEastArrowSouthWest
defaultPositions.northEastArrowSouthWest,
defaultPositions.northEastArrowSouthMiddleEast,
defaultPositions.northEastArrowSouthMiddleWest
];
}

/**
* Contextual toolbar configuration. Used by the {@link module:ui/toolbar/balloon/balloontoolbar~BalloonToolbar}
* feature.
*
* ## Configuring toolbar items
*
* const config = {
* balloonToolbar: [ 'bold', 'italic', 'undo', 'redo' ]
* };
Expand All @@ -318,5 +374,16 @@ function getBalloonPositions( isBackward ) {
*
* Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
*
* ## Configuring items grouping
*
* You can prevent automatic items grouping by setting the `shouldNotGroupWhenFull` option:
*
* const config = {
* balloonToolbar: {
* items: [ 'bold', 'italic', 'undo', 'redo' ]
* },
* shouldNotGroupWhenFull: true
* };
*
* @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#balloonToolbar
*/
33 changes: 32 additions & 1 deletion src/toolbar/toolbarview.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ export default class ToolbarView extends View {
*/
this.set( 'ariaLabel', t( 'Editor toolbar' ) );

/**
* The maximum width of the toolbar element.
*
* **Note**: When set to a specific value (e.g. `'200px'`), the value will affect the behavior of the
* {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}
* option by changing the number of {@link #items} that will be displayed in the toolbar at a time.
*
* @observable
* @default 'auto'
* @member {String} #maxWidth
*/
this.set( 'maxWidth', 'auto' );

/**
* A collection of toolbar items (buttons, dropdowns, etc.).
*
Expand Down Expand Up @@ -181,7 +194,10 @@ export default class ToolbarView extends View {
bind.if( 'isCompact', 'ck-toolbar_compact' )
],
role: 'toolbar',
'aria-label': bind.to( 'ariaLabel' )
'aria-label': bind.to( 'ariaLabel' ),
style: {
maxWidth: bind.to( 'maxWidth' )
}
},

children: this.children,
Expand Down Expand Up @@ -570,6 +586,7 @@ class DynamicGrouping {
this.viewElement = view.element;

this._enableGroupingOnResize();
this._enableGroupingOnMaxWidthChange( view );
}

/**
Expand Down Expand Up @@ -694,6 +711,18 @@ class DynamicGrouping {
this._updateGrouping();
}

/**
* Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
* it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
*
* @private
*/
_enableGroupingOnMaxWidthChange( view ) {
view.on( 'change:maxWidth', () => {
this._updateGrouping();
} );
}

/**
* When called, it will remove the last item from {@link #ungroupedItems} and move it back
* to the {@link #groupedItems} collection.
Expand Down Expand Up @@ -798,6 +827,8 @@ class DynamicGrouping {
* would normally wrap to the next line when there is not enough space to display them in a single row, for
* instance, if the parent container of the toolbar is narrow.
*
* Also see: {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth}.
*
* @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull
*/

Expand Down
13 changes: 11 additions & 2 deletions tests/manual/panel/balloon/balloonpanelview.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
padding: 5em;
}

h1 {
font-size: 22px;
text-align: center;
margin: 70px 0;
}

.target {
width: 125px;
height: 20px;
background: green;
margin: 0 0 0 100px;
background: lightblue;
transform: translateX(-50%);
margin-left: 50%;
}

.target + .target {
Expand All @@ -18,5 +25,7 @@

.ck-balloon-panel {
padding: .3em;
width: 250px;
text-align: center;
}
</style>
31 changes: 31 additions & 0 deletions tests/manual/panel/balloon/balloonpanelview.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,22 @@ import BalloonPanelView from '../../../../src/panel/balloon/balloonpanelview';
const defaultPositions = BalloonPanelView.defaultPositions;
const container = document.querySelector( '#container' );

let currentHeading = '';

for ( const i in defaultPositions ) {
const target = document.createElement( 'div' );
const heading = document.createElement( 'h1' );
const headingText = parseHeadingText( i );

heading.textContent = headingText;
target.classList.add( 'target' );

// Lazy heading
if ( currentHeading !== headingText ) {
container.appendChild( heading );
currentHeading = headingText;
}

container.appendChild( target );

const balloon = new BalloonPanelView();
Expand All @@ -27,3 +40,21 @@ for ( const i in defaultPositions ) {
]
} );
}

function parseHeadingText( text ) {
const normalizedText = getNormalizeHeading( text );
return getCapitalizedHeading( normalizedText );
}

// This helper function creates normalize heading text from a full name of the position,
// removing `ArrowXyz` part, like in the example:
// `southEastArrowNorthMiddleEast` -> `south East`.
function getNormalizeHeading( text ) {
return text
.replace( /(w*)arrow\w*/i, '$1' )
.replace( /([a-z])([A-Z])/, '$1 $2' );
}

function getCapitalizedHeading( text ) {
return text.charAt( 0 ).toUpperCase() + text.slice( 1 );
}
2 changes: 1 addition & 1 deletion tests/manual/panel/balloon/balloonpanelview.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## `BalloonPanelView` and `defaultPositions`

1. A number of green rectangles should be displayed in the page.
1. A number of colorful rectangles should be displayed in the page.
2. Each rectangle should have a panel attached.
3. Make sure the description in each panel matches the location of the panel.
Loading

0 comments on commit d36fd23

Please sign in to comment.