| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464 |
- /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
- */
- /**
- * @module ui/panel/balloon/contextualballoon
- */
- import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
- import BalloonPanelView from './balloonpanelview';
- import View from '@ckeditor/ckeditor5-ui/src/view';
- import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
- import Collection from '@ckeditor/ckeditor5-utils/src/collection';
- /**
- * Provides the common contextual balloon panel for the editor.
- *
- * This plugin allows reusing a single {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance
- * to display multiple contextual balloon panels in the editor.
- *
- * Child views of such a panel are stored in the stack and the last one in the stack is visible. When the
- * visible view is removed from the stack, the previous view becomes visible, etc. If there are no more
- * views in the stack, the balloon panel will hide.
- *
- * It simplifies managing the views and helps
- * avoid the unnecessary complexity of handling multiple {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
- * instances in the editor.
- *
- * @extends module:core/plugin~Plugin
- */
- export default class ContextualBalloon extends Plugin {
- /**
- * @inheritDoc
- */
- static get pluginName() {
- return 'ContextualBalloon';
- }
- /**
- * @inheritDoc
- */
- constructor( editor ) {
- super( editor );
- /**
- * The {@link module:utils/dom/position~Options#limiter position limiter}
- * for the {@link #view balloon}, used when no `limiter` has been passed into {@link #add}
- * or {@link #updatePosition}.
- *
- * By default, a function, which obtains the farthest DOM
- * {@link module:engine/view/rooteditableelement~RootEditableElement}
- * of the {@link module:engine/view/document~Document#selection}.
- *
- * @member {module:utils/dom/position~Options#limiter} #positionLimiter
- */
- this.positionLimiter = () => {
- const view = this.editor.editing.view;
- const viewDocument = view.document;
- const editableElement = viewDocument.selection.editableElement;
- if ( editableElement ) {
- return view.domConverter.mapViewToDom( editableElement.root );
- }
- return null;
- };
- /**
- * The currently visible view or `null` when there are no
- * views in the currently visible panel stack.
- *
- * @readonly
- * @observable
- * @member {module:ui/view~View|null} #visibleView
- */
- this.set( 'visibleView', null );
- /**
- * The common balloon panel view.
- *
- * @readonly
- * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
- */
- this.view = new BalloonPanelView( editor.locale );
- this.editor.ui.view.body.add( this.view );
- this.editor.ui.focusTracker.add( this.view.element );
- /**
- * Currently visible panel.
- *
- * @private
- * @readonly
- * @observable
- * @member {null|Object} #_visiblePanel
- */
- this.set( '_visiblePanel', null );
- /**
- * List of panels.
- *
- * @private
- * @type {module:utils/collection~Collection}
- */
- this._panels = new Collection();
- /**
- * @private
- * @type {Map.<Object>}
- */
- this._viewToPanel = new Map();
- /**
- * Rotator view embedded in the contextual balloon.
- * Displays currently visible view in the balloon and provides navigation for switching panels.
- *
- * @private
- * @type {module:ui/view~View}
- */
- this._rotatorView = this._createRotatorView();
- }
- /**
- * Returns `true` when the given view is in one of the stack. Otherwise returns `false`.
- *
- * @param {module:ui/view~View} view
- * @returns {Boolean}
- */
- hasView( view ) {
- return Array.from( this._viewToPanel.keys() ).includes( view );
- }
- /**
- * Adds a new view to the panel stack and makes it visible if current panel is visible.
- *
- * @param {Object} data Configuration of the view.
- * @param {String} [data.panelId='main'] Id of panel that view is added to.
- * @param {module:ui/view~View} [data.view] Content of the balloon.
- * @param {module:utils/dom/position~Options} [data.position] Positioning options.
- * @param {String} [data.balloonClassName] Additional CSS class added to the {@link #view balloon} when visible.
- * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
- */
- add( data ) {
- if ( this.hasView( data.view ) ) {
- /**
- * Trying to add configuration of the same view more than once.
- *
- * @error contextualballoon-add-view-exist
- */
- throw new CKEditorError( 'contextualballoon-add-view-exist: Cannot add configuration of the same view twice.' );
- }
- const panelId = data.panelId || 'main';
- // If new panel is added, creates it and show it.
- if ( !this._panels.has( panelId ) ) {
- this._panels.add( {
- id: panelId,
- stack: new Map( [ [ data.view, data ] ] )
- } );
- this._viewToPanel.set( data.view, this._panels.get( panelId ) );
- if ( !this._visiblePanel ) {
- this.showPanel( panelId );
- }
- return;
- }
- const panel = this._panels.get( panelId );
- // Add new view to the stack.
- panel.stack.set( data.view, data );
- this._viewToPanel.set( data.view, panel );
- // And display it if is added to the currently visible panel.
- if ( panel === this._visiblePanel ) {
- this._showView( data );
- }
- }
- /**
- * Removes the given view from the panel stack. If the removed view was visible,
- * then the view preceding it in panel the stack will become visible instead.
- * When there is no view in the stack then next panel will be displayed.
- * When there is not panel to display then balloon will hide.
- *
- * @param {module:ui/view~View} view A view to be removed from the balloon.
- */
- remove( view ) {
- if ( !this.hasView( view ) ) {
- /**
- * Trying to remove configuration of the view not defined in the stack.
- *
- * @error contextualballoon-remove-view-not-exist
- */
- throw new CKEditorError( 'contextualballoon-remove-view-not-exist: Cannot remove configuration of not existing view.' );
- }
- const panel = this._viewToPanel.get( view );
- // When visible view will be removed we need to show a preceding view or next panel
- // if a view is the only view in the panel.
- if ( this.visibleView === view ) {
- if ( panel.stack.size === 1 ) {
- if ( this._panels.length > 1 ) {
- this._showNextPanel();
- } else {
- this.view.unpin();
- this.visibleView = null;
- this._visiblePanel = null;
- this._rotatorView.content.clear();
- }
- } else {
- this._showView( Array.from( panel.stack.values() )[ panel.stack.size - 2 ] );
- }
- }
- if ( panel.stack.size === 1 ) {
- this._panels.remove( panel.id );
- } else {
- panel.stack.delete( view );
- }
- this._viewToPanel.delete( view );
- }
- /**
- * Updates the position of the balloon using position data of the first visible view in the panel stack.
- * When new position data is given then position data of currently visible panel will be updated.
- *
- * @param {module:utils/dom/position~Options} [position] position options.
- */
- updatePosition( position ) {
- if ( position ) {
- this._visiblePanel.stack.get( this.visibleView ).position = position;
- }
- this.view.pin( this._getBalloonPosition() );
- }
- /**
- * Shows last view from the stack of panel with given id.
- *
- * @param {String} id
- */
- showPanel( id ) {
- const panel = this._panels.get( id );
- if ( !panel ) {
- /**
- * Trying to show not existing panel.
- *
- * @error contextualballoon-showpanel-panel-not-exist
- */
- throw new CKEditorError( 'contextualballoon-showpanel-panel-not-exist: Cannot show not existing panel.' );
- }
- if ( this._visiblePanel === panel ) {
- return;
- }
- this._visiblePanel = panel;
- this._showView( Array.from( panel.stack.values() ).pop() );
- }
- /**
- * Shows last view from the stack of the next panel
- * according to the currently visible panel.
- *
- * @private
- */
- _showNextPanel() {
- if ( this._panels.length === 1 ) {
- return;
- }
- const panel = this._visiblePanel;
- let nextIndex = this._panels.getIndex( panel ) + 1;
- if ( !this._panels.get( nextIndex ) ) {
- nextIndex = 0;
- }
- this.showPanel( this._panels.get( nextIndex ).id );
- }
- /**
- * Shows last view from the stack of the previous panel
- * according to the currently visible panel.
- *
- * @private
- */
- _showPrevPanel() {
- if ( this._panels.length === 1 ) {
- return;
- }
- const panel = this._visiblePanel;
- let nextIndex = this._panels.getIndex( panel ) - 1;
- if ( !this._panels.get( nextIndex ) ) {
- nextIndex = this._panels.length - 1;
- }
- this.showPanel( this._panels.get( nextIndex ).id );
- }
- /**
- * Creates a rotator view.
- *
- * @private
- * @returns {~RotatorView}
- */
- _createRotatorView() {
- const view = new RotatorView( this.editor.locale );
- this.view.content.add( view );
- // Hide navigation when there is only a one panel.
- view.bind( 'isNavigationVisible' ).to( this._panels, 'length', value => value > 1 );
- // Show panels counter.
- view.bind( 'counter' ).to( this, '_visiblePanel', this._panels, 'length', ( panel, length ) => {
- if ( !panel ) {
- return `0/${ length }`;
- }
- return `${ this._panels.getIndex( panel ) + 1 }/${ length }`;
- } );
- view.buttonNextView.on( 'execute', () => this._showNextPanel() );
- view.buttonPrevView.on( 'execute', () => this._showPrevPanel() );
- return view;
- }
- /**
- * Sets the view as a content of the balloon and attaches balloon using position
- * options of the first view.
- *
- * @private
- * @param {Object} data Configuration.
- * @param {module:ui/view~View} [data.view] View to show in the balloon.
- * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
- * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
- */
- _showView( { view, balloonClassName = '', withArrow = true } ) {
- this.view.class = balloonClassName;
- this.view.withArrow = withArrow;
- this._rotatorView.content.clear();
- this._rotatorView.content.add( view );
- this.visibleView = view;
- this.view.pin( this._getBalloonPosition() );
- }
- /**
- * Returns position options of the last view in the stack.
- * This keeps the balloon in the same position when view is changed.
- *
- * @private
- * @returns {module:utils/dom/position~Options}
- */
- _getBalloonPosition() {
- let position = Array.from( this._visiblePanel.stack.values() ).pop().position;
- // Use the default limiter if none has been specified.
- if ( position && !position.limiter ) {
- // Don't modify the original options object.
- position = Object.assign( {}, position, {
- limiter: this.positionLimiter
- } );
- }
- return position;
- }
- }
- // Rotator view. Used to display last view from the panel stack.
- // Provides navigation buttons for switching panels.
- //
- // @private
- // @extends module:ui/view~View
- class RotatorView extends View {
- constructor( locale ) {
- super( locale );
- const bind = this.bindTemplate;
- // Defines whether navigation is visible or not.
- //
- // @member {Boolean} #isNavigationVisible
- this.set( 'isNavigationVisible', true );
- // Navigation button to switches panel to the next one.
- //
- // @type {module:ui/button/buttonview~ButtonView}
- this.buttonPrevView = this._createButtonView( locale.t( 'Prev' ) );
- // Navigation button to switches panel to the previous one.
- //
- // @type {module:ui/button/buttonview~ButtonView}
- this.buttonNextView = this._createButtonView( locale.t( 'Next' ) );
- // Collection of the child views which creates balloon panel contents.
- //
- // @readonly
- // @type {module:ui/viewcollection~ViewCollection}
- this.content = this.createCollection();
- this.setTemplate( {
- tag: 'div',
- attributes: {
- class: 'rotator'
- },
- children: [
- {
- tag: 'div',
- attributes: {
- class: [
- 'rotator_navigation',
- bind.to( 'isNavigationVisible', value => value ? '' : 'ck-hidden' )
- ]
- },
- children: [
- this.buttonPrevView,
- this.buttonNextView,
- {
- text: bind.to( 'counter' )
- }
- ]
- },
- {
- tag: 'div',
- attributes: {
- class: 'rotator_content'
- },
- children: this.content
- }
- ]
- } );
- }
- // Create navigation button view.
- //
- // @private
- // @param {String} label
- // @returns {module:ui/button/buttonview~ButtonView}
- _createButtonView( label ) {
- const view = new ButtonView( this.locale );
- view.set( {
- withText: true,
- label
- } );
- return view;
- }
- }
|