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 all 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
5 changes: 5 additions & 0 deletions docs/api/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ The widget API consists of two layers:
* The {@link module:widget/widget~Widget} plugin which enables base support for this feature. Usually, your plugin which implements a specific widget will define its reliance on the `Widget` plugin via its {@link module:core/plugin~Plugin.requires `Plugin.requires`} property.
* The {@link module:widget/utils~toWidget `toWidget()`} {@link module:widget/utils~toWidgetEditable `toWidgetEditable()`} functions which need to be used during the conversion in order to make a specific element either a widget or a widget's nested editable. See their documentation for more details.

Besides the above mentioned core functionalities, this package implements the following utils:

* The {@link module:widget/widgettoolbarrepository~WidgetToolbarRepository `WidgetToolbarRepository`} plugin which exposes a nice API for registering widget toolbars.
* A couple of helper functions for managing widgets in the {@link module:widget/utils `@ckeditor/ckeditor5-widget/utils`} module.

<info-box>
The widget API is proposed in a very different way than it was in CKEditor 4. It is just a set of utilities that allow you to implement typical object-like entities. Most of the work actually happens in the {@link api/engine engine} and this API's role is only to properly conduct the engine.
</info-box>
Expand Down
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
240 changes: 240 additions & 0 deletions src/widgettoolbarrepository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/**
* @module widget/widgettoolbarrepository
*/

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 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 ~WidgetToolbarRepository#register `WidgetToolbarRepository#register()`} method.
*
* The following example comes from the {@link module:image/imagetoolbar~ImageToolbar} plugin:
*
* class ImageToolbar extends Plugin {
* static get requires() {
* return [ WidgetToolbarRepository ];
* }
*
* afterInit() {
* const editor = this.editor;
* const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
*
* widgetToolbarRepository.register( 'image', {
* items: editor.config.get( 'image.toolbar' ),
* visibleWhen: viewSelection => isImageWidgetSelected( viewSelection )
* } );
* }
* }
*/
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 `items` 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~PluginInterface#afterInit `Plugin#afterInit()`}
* callback (or later) to make sure that the given toolbar items were already registered by other plugins.
*
* @param {String} toolbarId An id for the toolbar. Used to
* @param {Object} options
* @param {Array.<String>} options.items 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, { items, visibleWhen, balloonClassName = 'ck-toolbar-container' } ) {
const editor = this.editor;
const toolbarView = new ToolbarView();

if ( this._toolbars.has( toolbarId ) ) {
/**
* 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 } );
}

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

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

/**
* 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 the toolbar if the toolbar is not visible and repositions the toolbar's balloon when toolbar's
* view is the most top view in balloon stack.
*
* It might happen here that the toolbar's view is under another view. Then do nothing as the other toolbar view
* should be still visible after the {@link module:core/editor/editorui~EditorUI#event:update}.
*
* @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