This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Introduce the widget toolbar repository #54
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
6d6f600
WIP - WidgetToolbar.
ma2ciek 0b68ce1
Added API docs for the WidgetToolbar.
ma2ciek b16ff1b
Various fixes to widget toolbar.
ma2ciek fc82198
Added simple integration tests.
ma2ciek 08b3d46
Added tests for remove() method.
ma2ciek fa920a2
Added more tests.
ma2ciek 69927c9
Improved tests.
ma2ciek 4a0cfef
Code style improvements.
ma2ciek a2bd398
Improved tests.
ma2ciek b2f47b2
Code style improvements.
ma2ciek 39eba17
Cleaned up.
ma2ciek 95ac5f2
Changed WidgetToolbar to WidgetToolbarRepository.
ma2ciek 261ef60
Simplified default balloon class name.
ma2ciek ab05ffe
Changed isVisible to whenVisible.
ma2ciek 875bb8b
Fixed errors.
ma2ciek 6d5bf11
Fixed option name.
ma2ciek e4b1151
Removed deregister and isRegistered functions.
ma2ciek 9d46782
Improved API docs.
ma2ciek d7e9b2c
API docs fixes.
ma2ciek 4e9e587
API docs fixes.
ma2ciek 8c1bd96
API docs improvements.
ma2ciek 4c72221
Added missing docs.
ma2ciek e2cc5fc
Fixed failing tests on FF.
ma2ciek 3fe23d3
Fixed description in tests.
ma2ciek 1711a70
Changed order in the register function.
ma2ciek 0d72ab3
Renamed toolbarItems to items.
ma2ciek 031e370
Added API docs to the _showToolbar() method.
ma2ciek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} ); | ||
} | ||
} | ||
|
||
/** | ||
* @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 ) ); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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 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.
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.
So that's why there is
else if ( !this._balloon.hasView( toolbar.view ) )
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.
But maybe I don't see smth.
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.
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 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.
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.
I've added a description of this method.