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

Commit bc45176

Browse files
author
Piotr Jasiun
authored
Merge pull request #54 from ckeditor/t/ckeditor5-ui/442
Feature: Introduced the widget toolbar repository. Closes ckeditor/ckeditor5-ui#442.
2 parents 74ed902 + 031e370 commit bc45176

File tree

4 files changed

+590
-0
lines changed

4 files changed

+590
-0
lines changed

docs/api/widget.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ The widget API consists of two layers:
1919
* 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.
2020
* 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.
2121

22+
Besides the above mentioned core functionalities, this package implements the following utils:
23+
24+
* The {@link module:widget/widgettoolbarrepository~WidgetToolbarRepository `WidgetToolbarRepository`} plugin which exposes a nice API for registering widget toolbars.
25+
* A couple of helper functions for managing widgets in the {@link module:widget/utils `@ckeditor/ckeditor5-widget/utils`} module.
26+
2227
<info-box>
2328
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.
2429
</info-box>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"devDependencies": {
1919
"@ckeditor/ckeditor5-basic-styles": "^10.0.2",
20+
"@ckeditor/ckeditor5-editor-balloon": "^11.0.0",
2021
"@ckeditor/ckeditor5-editor-classic": "^11.0.0",
2122
"@ckeditor/ckeditor5-essentials": "^10.1.1",
2223
"@ckeditor/ckeditor5-paragraph": "^10.0.2",

src/widgettoolbarrepository.js

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/**
7+
* @module widget/widgettoolbarrepository
8+
*/
9+
10+
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11+
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
12+
import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
13+
import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
14+
import { isWidget } from './utils';
15+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
16+
17+
/**
18+
* Widget toolbar repository plugin. A central point for registering widget toolbars. This plugin handles the whole
19+
* toolbar rendering process and exposes a concise API.
20+
*
21+
* To add a toolbar for your widget use the {@link ~WidgetToolbarRepository#register `WidgetToolbarRepository#register()`} method.
22+
*
23+
* The following example comes from the {@link module:image/imagetoolbar~ImageToolbar} plugin:
24+
*
25+
* class ImageToolbar extends Plugin {
26+
* static get requires() {
27+
* return [ WidgetToolbarRepository ];
28+
* }
29+
*
30+
* afterInit() {
31+
* const editor = this.editor;
32+
* const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
33+
*
34+
* widgetToolbarRepository.register( 'image', {
35+
* items: editor.config.get( 'image.toolbar' ),
36+
* visibleWhen: viewSelection => isImageWidgetSelected( viewSelection )
37+
* } );
38+
* }
39+
* }
40+
*/
41+
export default class WidgetToolbarRepository extends Plugin {
42+
/**
43+
* @inheritDoc
44+
*/
45+
static get requires() {
46+
return [ ContextualBalloon ];
47+
}
48+
49+
/**
50+
* @inheritDoc
51+
*/
52+
static get pluginName() {
53+
return 'WidgetToolbarRepository';
54+
}
55+
56+
/**
57+
* @inheritDoc
58+
*/
59+
init() {
60+
const editor = this.editor;
61+
const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
62+
63+
// Disables the default balloon toolbar for all widgets.
64+
if ( balloonToolbar ) {
65+
this.listenTo( balloonToolbar, 'show', evt => {
66+
if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
67+
evt.stop();
68+
}
69+
}, { priority: 'high' } );
70+
}
71+
72+
/**
73+
* A map of toolbars.
74+
*
75+
* @protected
76+
* @member {Map.<string,Object>} #_toolbars
77+
*/
78+
this._toolbars = new Map();
79+
80+
/**
81+
* @private
82+
*/
83+
this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
84+
85+
this.listenTo( editor.ui, 'update', () => {
86+
this._updateToolbarsVisibility();
87+
} );
88+
89+
// UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
90+
this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
91+
this._updateToolbarsVisibility();
92+
}, { priority: 'low' } );
93+
}
94+
95+
/**
96+
* Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
97+
* `visibleWhen` function. Toolbar items are gathered from `items` array.
98+
* The balloon's CSS class is by default `ck-toolbar-container` and may be override with the `balloonClassName` option.
99+
*
100+
* Note: This method should be called in the {@link module:core/plugin~PluginInterface#afterInit `Plugin#afterInit()`}
101+
* callback (or later) to make sure that the given toolbar items were already registered by other plugins.
102+
*
103+
* @param {String} toolbarId An id for the toolbar. Used to
104+
* @param {Object} options
105+
* @param {Array.<String>} options.items Array of toolbar items.
106+
* @param {Function} options.visibleWhen Callback which specifies when the toolbar should be visible for the widget.
107+
* @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
108+
*/
109+
register( toolbarId, { items, visibleWhen, balloonClassName = 'ck-toolbar-container' } ) {
110+
const editor = this.editor;
111+
const toolbarView = new ToolbarView();
112+
113+
if ( this._toolbars.has( toolbarId ) ) {
114+
/**
115+
* Toolbar with the given id was already added.
116+
*
117+
* @error widget-toolbar-duplicated
118+
* @param toolbarId Toolbar id.
119+
*/
120+
throw new CKEditorError( 'widget-toolbar-duplicated: Toolbar with the given id was already added.', { toolbarId } );
121+
}
122+
123+
toolbarView.fillFromConfig( items, editor.ui.componentFactory );
124+
125+
this._toolbars.set( toolbarId, {
126+
view: toolbarView,
127+
visibleWhen,
128+
balloonClassName,
129+
} );
130+
}
131+
132+
/**
133+
* Iterates over stored toolbars and makes them visible or hidden.
134+
*
135+
* @private
136+
*/
137+
_updateToolbarsVisibility() {
138+
for ( const toolbar of this._toolbars.values() ) {
139+
if ( !this.editor.ui.focusTracker.isFocused || !toolbar.visibleWhen( this.editor.editing.view.document.selection ) ) {
140+
this._hideToolbar( toolbar );
141+
} else {
142+
this._showToolbar( toolbar );
143+
}
144+
}
145+
}
146+
147+
/**
148+
* Hides the given toolbar.
149+
*
150+
* @private
151+
* @param {Object} toolbar
152+
*/
153+
_hideToolbar( toolbar ) {
154+
if ( !this._isToolbarVisible( toolbar ) ) {
155+
return;
156+
}
157+
158+
this._balloon.remove( toolbar.view );
159+
}
160+
161+
/**
162+
* Shows up the toolbar if the toolbar is not visible and repositions the toolbar's balloon when toolbar's
163+
* view is the most top view in balloon stack.
164+
*
165+
* It might happen here that the toolbar's view is under another view. Then do nothing as the other toolbar view
166+
* should be still visible after the {@link module:core/editor/editorui~EditorUI#event:update}.
167+
*
168+
* @private
169+
* @param {Object} toolbar
170+
*/
171+
_showToolbar( toolbar ) {
172+
if ( this._isToolbarVisible( toolbar ) ) {
173+
repositionContextualBalloon( this.editor );
174+
} else if ( !this._balloon.hasView( toolbar.view ) ) {
175+
this._balloon.add( {
176+
view: toolbar.view,
177+
position: getBalloonPositionData( this.editor ),
178+
balloonClassName: toolbar.balloonClassName,
179+
} );
180+
}
181+
}
182+
183+
/**
184+
* @private
185+
* @param {Object} toolbar
186+
*/
187+
_isToolbarVisible( toolbar ) {
188+
return this._balloon.visibleView == toolbar.view;
189+
}
190+
}
191+
192+
function repositionContextualBalloon( editor ) {
193+
const balloon = editor.plugins.get( 'ContextualBalloon' );
194+
const position = getBalloonPositionData( editor );
195+
196+
balloon.updatePosition( position );
197+
}
198+
199+
function getBalloonPositionData( editor ) {
200+
const editingView = editor.editing.view;
201+
const defaultPositions = BalloonPanelView.defaultPositions;
202+
const widget = getParentWidget( editingView.document.selection );
203+
204+
return {
205+
target: editingView.domConverter.viewToDom( widget ),
206+
positions: [
207+
defaultPositions.northArrowSouth,
208+
defaultPositions.northArrowSouthWest,
209+
defaultPositions.northArrowSouthEast,
210+
defaultPositions.southArrowNorth,
211+
defaultPositions.southArrowNorthWest,
212+
defaultPositions.southArrowNorthEast
213+
]
214+
};
215+
}
216+
217+
function getParentWidget( selection ) {
218+
const selectedElement = selection.getSelectedElement();
219+
220+
if ( selectedElement && isWidget( selectedElement ) ) {
221+
return selectedElement;
222+
}
223+
224+
const position = selection.getFirstPosition();
225+
let parent = position.parent;
226+
227+
while ( parent ) {
228+
if ( parent.is( 'element' ) && isWidget( parent ) ) {
229+
return parent;
230+
}
231+
232+
parent = parent.parent;
233+
}
234+
}
235+
236+
function isWidgetSelected( selection ) {
237+
const viewElement = selection.getSelectedElement();
238+
239+
return !!( viewElement && isWidget( viewElement ) );
240+
}

0 commit comments

Comments
 (0)