-
Notifications
You must be signed in to change notification settings - Fork 6
Introduce the widget toolbar repository #54
Changes from 16 commits
6d6f600
0b68ce1
b16ff1b
fc82198
08b3d46
fa920a2
69927c9
4a0cfef
a2bd398
b2f47b2
39eba17
95ac5f2
261ef60
ab05ffe
875bb8b
6d5bf11
e4b1151
9d46782
d7e9b2c
4e9e587
8c1bd96
4c72221
e2cc5fc
3fe23d3
1711a70
0d72ab3
031e370
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
* | ||
* 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to be updated. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about removing the toolbar on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also think we could remove these methods ( |
||
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, | ||
} ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So that's why there is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But maybe I don't see smth. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It means that if the view was already added but is not on top nothing happen (including no error). Is it correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ) ); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.