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

Introduce the widget toolbar repository #54

Merged
merged 27 commits into from
Sep 20, 2018
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6d6f600
WIP - WidgetToolbar.
ma2ciek Sep 13, 2018
0b68ce1
Added API docs for the WidgetToolbar.
ma2ciek Sep 13, 2018
b16ff1b
Various fixes to widget toolbar.
ma2ciek Sep 14, 2018
fc82198
Added simple integration tests.
ma2ciek Sep 18, 2018
08b3d46
Added tests for remove() method.
ma2ciek Sep 18, 2018
fa920a2
Added more tests.
ma2ciek Sep 18, 2018
69927c9
Improved tests.
ma2ciek Sep 18, 2018
4a0cfef
Code style improvements.
ma2ciek Sep 18, 2018
a2bd398
Improved tests.
ma2ciek Sep 18, 2018
b2f47b2
Code style improvements.
ma2ciek Sep 18, 2018
39eba17
Cleaned up.
ma2ciek Sep 18, 2018
95ac5f2
Changed WidgetToolbar to WidgetToolbarRepository.
ma2ciek Sep 19, 2018
261ef60
Simplified default balloon class name.
ma2ciek Sep 19, 2018
ab05ffe
Changed isVisible to whenVisible.
ma2ciek Sep 19, 2018
875bb8b
Fixed errors.
ma2ciek Sep 19, 2018
6d5bf11
Fixed option name.
ma2ciek Sep 19, 2018
e4b1151
Removed deregister and isRegistered functions.
ma2ciek Sep 19, 2018
9d46782
Improved API docs.
ma2ciek Sep 19, 2018
d7e9b2c
API docs fixes.
ma2ciek Sep 19, 2018
4e9e587
API docs fixes.
ma2ciek Sep 19, 2018
8c1bd96
API docs improvements.
ma2ciek Sep 19, 2018
4c72221
Added missing docs.
ma2ciek Sep 19, 2018
e2cc5fc
Fixed failing tests on FF.
ma2ciek Sep 19, 2018
3fe23d3
Fixed description in tests.
ma2ciek Sep 19, 2018
1711a70
Changed order in the register function.
ma2ciek Sep 19, 2018
0d72ab3
Renamed toolbarItems to items.
ma2ciek Sep 20, 2018
031e370
Added API docs to the _showToolbar() method.
ma2ciek Sep 20, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"devDependencies": {
"@ckeditor/ckeditor5-basic-styles": "^10.0.2",
"@ckeditor/ckeditor5-editor-balloon": "^11.0.0",
"@ckeditor/ckeditor5-editor-classic": "^11.0.0",
"@ckeditor/ckeditor5-essentials": "^10.1.1",
"@ckeditor/ckeditor5-paragraph": "^10.0.2",
Expand Down
261 changes: 261 additions & 0 deletions src/widgettoolbarrepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
import { isWidget } from './utils';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';

/**
* Widget toolbar repository plugin. A central point for creating widget toolbars. This plugin handles the whole
* toolbar rendering process and exposes concise API.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Widget toolbar repository plugin. A central point for registering widget toolbars. This plugin handles the whole
toolbar rendering process and exposes a concise API.

To add a toolbar for your widget use the {@link module:widget/widgettoolbarrepository~WidgetToolbarRepository#register `WidgetToolbarRepository#register()`} method.

The following example comes from the {@link module:image/imagetoolbar~ImageToolbar} plugin:

*
* Creating toolbar for the widget bases on the {@link ~register()} method.
*
* This plugin adds to the plugin list directly or indirectly prevents showing up
* the {@link module:ui/toolbar/balloontoolbar~BalloonToolbar} toolbar and the widget toolbar at the same time.
*
* Usage example comes from {@link module:image/imagetoolbar~ImageToolbar}:
*
* class ImageToolbar extends Plugin {
* static get requires() {
* return [ WidgetToolbarRepository ];
* }
*
* afterInit() {
* const editor = this.editor;
* const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
*
* widgetToolbarRepository.add( {
* toolbarItems: editor.config.get( 'image.toolbar' )
* visibleWhen: isImageWidgetSelected
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to be updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

* } );
* }
* }
*/
export default class WidgetToolbarRepository extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ ContextualBalloon ];
}

/**
* @inheritDoc
*/
static get pluginName() {
return 'WidgetToolbarRepository';
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;
const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );

// Disables the default balloon toolbar for all widgets.
if ( balloonToolbar ) {
this.listenTo( balloonToolbar, 'show', evt => {
if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
evt.stop();
}
}, { priority: 'high' } );
}

/**
* A map of toolbars.
*
* @protected
* @member {Map.<string,Object>} #_toolbars
*/
this._toolbars = new Map();

/**
* @private
*/
this._balloon = this.editor.plugins.get( 'ContextualBalloon' );

this.listenTo( editor.ui, 'update', () => {
this._updateToolbarsVisibility();
} );

// UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
this._updateToolbarsVisibility();
}, { priority: 'low' } );
}

/**
* Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
* `visibleWhen` function. Toolbar items are gathered from `toolbarItems` array.
* The balloon's CSS class is by default `ck-toolbar-container` and may be override with the `balloonClassName` option.
*
* Note: This method should be called in the {@link module:core/plugin/Plugin~afterInit} to make sure that plugins for toolbar items
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This method should be called in the {@link module:core/plugin/Plugin#afterInit `Plugin#afterInit()`} callback (or later) to make sure that the given toolbar items were already registered by other plugins.

* will be already loaded and available in the UI component factory.
*
* @param {String} toolbarId An id for the toolbar. Used to
* @param {Object} options
* @param {Array.<String>} options.toolbarItems Array of toolbar items.
* @param {Function} options.visibleWhen Callback which specifies when the toolbar should be visible for the widget.
* @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
*/
register( toolbarId, { toolbarItems, visibleWhen, balloonClassName = 'ck-toolbar-container' } ) {
const editor = this.editor;
const toolbarView = new ToolbarView();

toolbarView.fillFromConfig( toolbarItems, editor.ui.componentFactory );

if ( this._toolbars.has( toolbarId ) ) {
Copy link

@pjasiun pjasiun Sep 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check could be moved to the beginning of this method.

/**
* Toolbar with the given id was already added.
*
* @error widget-toolbar-duplicated
* @param toolbarId Toolbar id.
*/
throw new CKEditorError( 'widget-toolbar-duplicated: Toolbar with the given id was already added.', { toolbarId } );
}

this._toolbars.set( toolbarId, {
view: toolbarView,
visibleWhen,
balloonClassName,
} );
}

/**
* Removes toolbar with the given toolbarId.
*
* @param {String} toolbarId Toolbar identificator.
*/
deregister( toolbarId ) {
Copy link

@pjasiun pjasiun Sep 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be deregister or unregister? Also, I think we do not need this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about removing the toolbar on the destroy() method. But it's questionable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think we could remove these methods (deregister() and isRegistered()) for now. Unless any of them are used somewhere.

const toolbar = this._toolbars.get( toolbarId );

if ( !toolbar ) {
/**
* Toolbar with the given id was already added.
*
* @error widget-toolbar-does-not-exist
* @param toolbarId Toolbar id.
*/
throw new CKEditorError( 'widget-toolbar-does-not-exist', { toolbarId } );
}

this._hideToolbar( toolbar );
this._toolbars.delete( toolbarId );
}

/**
* Returns `true` when a toolbar with the given id is present in the widget toolbar repository.
*
* @param {String} toolbarId Toolbar identificator.
*/
isRegistered( toolbarId ) {
return this._toolbars.has( toolbarId );
}

/**
* Iterates over stored toolbars and makes them visible or hidden.
*
* @private
*/
_updateToolbarsVisibility() {
for ( const toolbar of this._toolbars.values() ) {
if ( !this.editor.ui.focusTracker.isFocused || !toolbar.visibleWhen( this.editor.editing.view.document.selection ) ) {
this._hideToolbar( toolbar );
} else {
this._showToolbar( toolbar );
}
}
}

/**
* Hides the given toolbar.
*
* @private
* @param {Object} toolbar
*/
_hideToolbar( toolbar ) {
if ( !this._isToolbarVisible( toolbar ) ) {
return;
}

this._balloon.remove( toolbar.view );
}

/**
* Shows up or repositions the given toolbar.
*
* @private
* @param {Object} toolbar
*/
_showToolbar( toolbar ) {
if ( this._isToolbarVisible( toolbar ) ) {
repositionContextualBalloon( this.editor );
} else if ( !this._balloon.hasView( toolbar.view ) ) {
this._balloon.add( {
view: toolbar.view,
position: getBalloonPositionData( this.editor ),
balloonClassName: toolbar.balloonClassName,
} );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure, but if the toolbar was already added to the balloon, shouldn't we just move it on top instead of adding it again? //cc @oleq

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that contextual balloon throw when you add the same view twice (https://github.com/ckeditor/ckeditor5-ui/blob/85920b547c5d20ab4fcc1d495b19498c23a692d8/src/panel/balloon/contextualballoon.js#L126). I think it will be saver to remove the view first in such case (if the view was already added but is not on the top) and then re-add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that's why there is else if ( !this._balloon.hasView( toolbar.view ) )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But maybe I don't see smth.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that's why there is else if ( !this._balloon.hasView( toolbar.view ) )

It means that if the view was already added but is not on top nothing happen (including no error). Is it correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK yes 😄

We should reposition or show the view only if it's the most top view in the stack.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a description of this method.

}
}

/**
* @private
* @param {Object} toolbar
*/
_isToolbarVisible( toolbar ) {
return this._balloon.visibleView == toolbar.view;
}
}

function repositionContextualBalloon( editor ) {
const balloon = editor.plugins.get( 'ContextualBalloon' );
const position = getBalloonPositionData( editor );

balloon.updatePosition( position );
}

function getBalloonPositionData( editor ) {
const editingView = editor.editing.view;
const defaultPositions = BalloonPanelView.defaultPositions;
const widget = getParentWidget( editingView.document.selection );

return {
target: editingView.domConverter.viewToDom( widget ),
positions: [
defaultPositions.northArrowSouth,
defaultPositions.northArrowSouthWest,
defaultPositions.northArrowSouthEast,
defaultPositions.southArrowNorth,
defaultPositions.southArrowNorthWest,
defaultPositions.southArrowNorthEast
]
};
}

function getParentWidget( selection ) {
const selectedElement = selection.getSelectedElement();

if ( selectedElement && isWidget( selectedElement ) ) {
return selectedElement;
}

const position = selection.getFirstPosition();
let parent = position.parent;

while ( parent ) {
if ( parent.is( 'element' ) && isWidget( parent ) ) {
return parent;
}

parent = parent.parent;
}
}

function isWidgetSelected( selection ) {
const viewElement = selection.getSelectedElement();

return !!( viewElement && isWidget( viewElement ) );
}
Loading