| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- /**
- * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @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' ),
- * getRelatedElement: getSelectedImageWidget
- * } );
- * }
- * }
- */
- export default class WidgetToolbarRepository extends Plugin {
- /**
- * @inheritDoc
- */
- static get requires() {
- return [ ContextualBalloon ];
- }
- /**
- * @inheritDoc
- */
- static get pluginName() {
- return 'WidgetToolbarRepository';
- }
- /**
- * @inheritDoc
- */
- init() {
- const editor = this.editor;
- // Disables the default balloon toolbar for all widgets.
- if ( editor.plugins.has( 'BalloonToolbar' ) ) {
- const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
- this.listenTo( balloonToolbar, 'show', evt => {
- if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
- evt.stop();
- }
- }, { priority: 'high' } );
- }
- /**
- * A map of toolbar definitions.
- *
- * @protected
- * @member {Map.<String,module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition>} #_toolbarDefinitions
- */
- this._toolbarDefinitions = new Map();
- /**
- * @private
- */
- this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
- this.on( 'change:isEnabled', () => {
- this._updateToolbarsVisibility();
- } );
- 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' } );
- }
- destroy() {
- super.destroy();
- for ( const toolbarConfig of this._toolbarDefinitions.values() ) {
- toolbarConfig.view.destroy();
- }
- }
- /**
- * Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
- * `getRelatedElement` 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 {String} [options.ariaLabel] Label used by assistive technologies to describe this toolbar element.
- * @param {Array.<String>} options.items Array of toolbar items.
- * @param {Function} options.getRelatedElement Callback which returns an element the toolbar should be attached to.
- * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
- */
- register( toolbarId, { ariaLabel, items, getRelatedElement, balloonClassName = 'ck-toolbar-container' } ) {
- const editor = this.editor;
- const t = editor.t;
- const toolbarView = new ToolbarView( editor.locale );
- toolbarView.ariaLabel = ariaLabel || t( 'Widget toolbar' );
- if ( this._toolbarDefinitions.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.', this, { toolbarId } );
- }
- toolbarView.fillFromConfig( items, editor.ui.componentFactory );
- this._toolbarDefinitions.set( toolbarId, {
- view: toolbarView,
- getRelatedElement,
- balloonClassName
- } );
- }
- /**
- * Iterates over stored toolbars and makes them visible or hidden.
- *
- * @private
- */
- _updateToolbarsVisibility() {
- let maxRelatedElementDepth = 0;
- let deepestRelatedElement = null;
- let deepestToolbarDefinition = null;
- for ( const definition of this._toolbarDefinitions.values() ) {
- const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );
- if ( !this.isEnabled || !relatedElement ) {
- if ( this._isToolbarInBalloon( definition ) ) {
- this._hideToolbar( definition );
- }
- } else if ( !this.editor.ui.focusTracker.isFocused ) {
- if ( this._isToolbarVisible( definition ) ) {
- this._hideToolbar( definition );
- }
- } else {
- const relatedElementDepth = relatedElement.getAncestors().length;
- // Many toolbars can express willingness to be displayed but they do not know about
- // each other. Figure out which toolbar is deepest in the view tree to decide which
- // should be displayed. For instance, if a selected image is inside a table cell, display
- // the ImageToolbar rather than the TableToolbar (#60).
- if ( relatedElementDepth > maxRelatedElementDepth ) {
- maxRelatedElementDepth = relatedElementDepth;
- deepestRelatedElement = relatedElement;
- deepestToolbarDefinition = definition;
- }
- }
- }
- if ( deepestToolbarDefinition ) {
- this._showToolbar( deepestToolbarDefinition, deepestRelatedElement );
- }
- }
- /**
- * Hides the given toolbar.
- *
- * @private
- * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
- */
- _hideToolbar( toolbarDefinition ) {
- this._balloon.remove( toolbarDefinition.view );
- this.stopListening( this._balloon, 'change:visibleView' );
- }
- /**
- * Shows up the toolbar if the toolbar is not visible.
- * Otherwise, 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 {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
- * @param {module:engine/view/element~Element} relatedElement
- */
- _showToolbar( toolbarDefinition, relatedElement ) {
- if ( this._isToolbarVisible( toolbarDefinition ) ) {
- repositionContextualBalloon( this.editor, relatedElement );
- } else if ( !this._isToolbarInBalloon( toolbarDefinition ) ) {
- this._balloon.add( {
- view: toolbarDefinition.view,
- position: getBalloonPositionData( this.editor, relatedElement ),
- balloonClassName: toolbarDefinition.balloonClassName
- } );
- // Update toolbar position each time stack with toolbar view is switched to visible.
- // This is in a case target element has changed when toolbar was in invisible stack
- // e.g. target image was wrapped by a block quote.
- // See https://github.com/ckeditor/ckeditor5-widget/issues/92.
- this.listenTo( this._balloon, 'change:visibleView', () => {
- for ( const definition of this._toolbarDefinitions.values() ) {
- if ( this._isToolbarVisible( definition ) ) {
- const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );
- repositionContextualBalloon( this.editor, relatedElement );
- }
- }
- } );
- }
- }
- /**
- * @private
- * @param {Object} toolbar
- * @returns {Boolean}
- */
- _isToolbarVisible( toolbar ) {
- return this._balloon.visibleView === toolbar.view;
- }
- /**
- * @private
- * @param {Object} toolbar
- * @returns {Boolean}
- */
- _isToolbarInBalloon( toolbar ) {
- return this._balloon.hasView( toolbar.view );
- }
- }
- function repositionContextualBalloon( editor, relatedElement ) {
- const balloon = editor.plugins.get( 'ContextualBalloon' );
- const position = getBalloonPositionData( editor, relatedElement );
- balloon.updatePosition( position );
- }
- function getBalloonPositionData( editor, relatedElement ) {
- const editingView = editor.editing.view;
- const defaultPositions = BalloonPanelView.defaultPositions;
- return {
- target: editingView.domConverter.mapViewToDom( relatedElement ),
- positions: [
- defaultPositions.northArrowSouth,
- defaultPositions.northArrowSouthWest,
- defaultPositions.northArrowSouthEast,
- defaultPositions.southArrowNorth,
- defaultPositions.southArrowNorthWest,
- defaultPositions.southArrowNorthEast
- ]
- };
- }
- function isWidgetSelected( selection ) {
- const viewElement = selection.getSelectedElement();
- return !!( viewElement && isWidget( viewElement ) );
- }
- /**
- * The toolbar definition object used by the toolbar repository to manage toolbars.
- * It contains information necessary to display the toolbar in the
- * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} and
- * update it during its life (display) cycle.
- *
- * @typedef {Object} module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition
- *
- * @property {module:ui/view~View} view The UI view of the toolbar.
- * @property {Function} getRelatedElement A function that returns an engine {@link module:engine/view/view~View}
- * element the toolbar is to be attached to. For instance, an image widget or a table widget (or `null` when
- * there is no such element). The function accepts an instance of {@link module:engine/view/selection~Selection}.
- * @property {String} balloonClassName CSS class for the widget balloon when a toolbar is displayed.
- */
|