|
|
@@ -9,22 +9,54 @@
|
|
|
|
|
|
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
|
|
import BalloonPanelView from './balloonpanelview';
|
|
|
+import View from '../../view';
|
|
|
+import ButtonView from '../../button/buttonview';
|
|
|
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
|
|
|
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
|
|
|
+import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
|
|
|
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
|
|
|
+
|
|
|
+import prevIcon from '../../../theme/icons/previous-arrow.svg';
|
|
|
+import nextIcon from '../../../theme/icons/next-arrow.svg';
|
|
|
+
|
|
|
+import '../../../theme/components/panel/balloonrotator.css';
|
|
|
+import '../../../theme/components/panel/fakepanel.css';
|
|
|
+
|
|
|
+const toPx = toUnit( 'px' );
|
|
|
|
|
|
/**
|
|
|
- * 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.
|
|
|
+ * Provides the common contextual balloon for 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
|
|
|
+ * The role of this plugin is to unified contextual balloons logic, simplifies managing the views and helps
|
|
|
* avoid the unnecessary complexity of handling multiple {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
|
|
|
* instances in the editor.
|
|
|
*
|
|
|
+ * This plugin allows creating single or multiple panel stacks.
|
|
|
+ *
|
|
|
+ * Each stack may have multiple views, the one on the top is visible. When the visible view is removed from the stack,
|
|
|
+ * the previous view becomes visible, etc. It might be useful to implement nested navigation in a balloon. For instance
|
|
|
+ * toolbar view may have a link button. When you click it, link view (which let you set the URL) is created and put on
|
|
|
+ * top of the toolbar view, so not link panel is displayed. When you finish editing link and close (remove) link view,
|
|
|
+ * the toolbar view is visible back.
|
|
|
+ *
|
|
|
+ * However, there are cases when there are multiple independent balloons to be displayed. For instance, if the selection
|
|
|
+ * is inside two inline comments at the same time. For such cases, you can create two independent panel stacks.
|
|
|
+ * Contextual balloon plugin will create a navigation bar to let users switch between these panel stacks - "next balloon"
|
|
|
+ * and "previous balloon" buttons.
|
|
|
+ *
|
|
|
+ * If there are no views in the current stack, the balloon panel will try to switch to the next stack. If there are no
|
|
|
+ * panels in any stack then balloon panel will be hidden.
|
|
|
+ *
|
|
|
+ * **Note**: To force balloon panel to show only one view - even if there are other stacks - use `singleViewMode=true` option
|
|
|
+ * when {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#add adding} view to a panel.
|
|
|
+ *
|
|
|
+ * From the implementation point of view, contextual ballon plugin is reusing a single
|
|
|
+ * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance to display multiple contextual balloon
|
|
|
+ * panels in the editor. It also creates a special {@link module:ui/panel/balloon/contextualballoon~RotatorView rotator view},
|
|
|
+ * used to manage multiple panel stacks. Rotator view is a child of the balloon panel view and the parent of the specific
|
|
|
+ * view you want to display. If there is are more than one panel stack to be displayed, the rotator view will add a
|
|
|
+ * navigation bar. If there is only one stack, rotator view is transparent (do not add any UI elements).
|
|
|
+ *
|
|
|
* @extends module:core/plugin~Plugin
|
|
|
*/
|
|
|
export default class ContextualBalloon extends Plugin {
|
|
|
@@ -38,14 +70,8 @@ export default class ContextualBalloon extends Plugin {
|
|
|
/**
|
|
|
* @inheritDoc
|
|
|
*/
|
|
|
- init() {
|
|
|
- /**
|
|
|
- * The common balloon panel view.
|
|
|
- *
|
|
|
- * @readonly
|
|
|
- * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
|
|
|
- */
|
|
|
- this.view = new BalloonPanelView();
|
|
|
+ constructor( editor ) {
|
|
|
+ super( editor );
|
|
|
|
|
|
/**
|
|
|
* The {@link module:utils/dom/position~Options#limiter position limiter}
|
|
|
@@ -71,51 +97,99 @@ export default class ContextualBalloon extends Plugin {
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * Stack of the views injected into the balloon. Last one in the stack is displayed
|
|
|
- * as a content of {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#view}.
|
|
|
+ * The currently visible view or `null` when there are no views in the any 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 );
|
|
|
+ editor.ui.view.body.add( this.view );
|
|
|
+ editor.ui.focusTracker.add( this.view.element );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Map of views and its stacks.
|
|
|
*
|
|
|
* @private
|
|
|
- * @member {Map} #_stack
|
|
|
+ * @type {Map.<module:ui/view~View,Set>}
|
|
|
*/
|
|
|
- this._stack = new Map();
|
|
|
+ this._viewToStack = new Map();
|
|
|
|
|
|
- // Add balloon panel view to editor `body` collection and wait until view will be ready.
|
|
|
- this.editor.ui.view.body.add( this.view );
|
|
|
+ /**
|
|
|
+ * Map of ids and stacks.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {Map.<String,Set>}
|
|
|
+ */
|
|
|
+ this._idToStack = new Map();
|
|
|
|
|
|
- // Editor should be focused when contextual balloon is focused.
|
|
|
- this.editor.ui.focusTracker.add( this.view.element );
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * A total number of all stacks in the balloon.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @readonly
|
|
|
+ * @observable
|
|
|
+ * @member {Number} #_numberOfStacks
|
|
|
+ */
|
|
|
+ this.set( '_numberOfStacks', 0 );
|
|
|
|
|
|
- /**
|
|
|
- * Returns the currently visible view or `null` when there are no
|
|
|
- * views in the stack.
|
|
|
- *
|
|
|
- * @returns {module:ui/view~View|null}
|
|
|
- */
|
|
|
- get visibleView() {
|
|
|
- const item = this._stack.get( this.view.content.get( 0 ) );
|
|
|
+ /**
|
|
|
+ * Flag that controls the single view mode.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @readonly
|
|
|
+ * @observable
|
|
|
+ * @member {Boolean} #_singleViewMode
|
|
|
+ */
|
|
|
+ this.set( '_singleViewMode', false );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Rotator view embedded in the contextual balloon.
|
|
|
+ * Displays currently visible view in the balloon and provides navigation for switching stacks.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {module:ui/panel/balloon/contextualballoon~RotatorView}
|
|
|
+ */
|
|
|
+ this._rotatorView = this._createRotatorView();
|
|
|
|
|
|
- return item ? item.view : null;
|
|
|
+ /**
|
|
|
+ * Displays fake panels under the balloon panel view when multiple stacks are added to the balloon.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {module:ui/view~View}
|
|
|
+ */
|
|
|
+ this._fakePanelsView = this._createFakePanelsView();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Returns `true` when the given view is in the stack. Otherwise returns `false`.
|
|
|
+ * 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 this._stack.has( view );
|
|
|
+ return Array.from( this._viewToStack.keys() ).includes( view );
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Adds a new view to the stack and makes it visible.
|
|
|
+ * Adds a new view to the stack and makes it visible if current stack is visible
|
|
|
+ * or it is a first view in the balloon.
|
|
|
*
|
|
|
* @param {Object} data Configuration of the view.
|
|
|
+ * @param {String} [data.stackId='main'] Id of a stack 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.
|
|
|
+ * @param {Boolean} [data.singleViewMode=false] Whether the view should be only visible view - even if other stacks were added.
|
|
|
*/
|
|
|
add( data ) {
|
|
|
if ( this.hasView( data.view ) ) {
|
|
|
@@ -127,23 +201,42 @@ export default class ContextualBalloon extends Plugin {
|
|
|
throw new CKEditorError( 'contextualballoon-add-view-exist: Cannot add configuration of the same view twice.' );
|
|
|
}
|
|
|
|
|
|
- // When adding view to the not empty balloon.
|
|
|
- if ( this.visibleView ) {
|
|
|
- // Remove displayed content from the view.
|
|
|
- this.view.content.remove( this.visibleView );
|
|
|
+ const stackId = data.stackId || 'main';
|
|
|
+
|
|
|
+ // If new stack is added, creates it and show view from this stack.
|
|
|
+ if ( !this._idToStack.has( stackId ) ) {
|
|
|
+ this._idToStack.set( stackId, new Map( [ [ data.view, data ] ] ) );
|
|
|
+ this._viewToStack.set( data.view, this._idToStack.get( stackId ) );
|
|
|
+ this._numberOfStacks = this._idToStack.size;
|
|
|
+
|
|
|
+ if ( !this._visibleStack || data.singleViewMode ) {
|
|
|
+ this.showStack( stackId );
|
|
|
+ }
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const stack = this._idToStack.get( stackId );
|
|
|
+
|
|
|
+ if ( data.singleViewMode ) {
|
|
|
+ this.showStack( stackId );
|
|
|
}
|
|
|
|
|
|
// Add new view to the stack.
|
|
|
- this._stack.set( data.view, data );
|
|
|
+ stack.set( data.view, data );
|
|
|
+ this._viewToStack.set( data.view, stack );
|
|
|
|
|
|
- // And display it.
|
|
|
- this._show( data );
|
|
|
+ // And display it if is added to the currently visible stack.
|
|
|
+ if ( stack === this._visibleStack ) {
|
|
|
+ this._showView( data );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Removes the given view from the stack. If the removed view was visible,
|
|
|
* then the view preceding it in the stack will become visible instead.
|
|
|
- * When there is no view in the stack then balloon will hide.
|
|
|
+ * When there is no view in the stack then next stack will be displayed.
|
|
|
+ * When there is not more stacks then balloon will hide.
|
|
|
*
|
|
|
* @param {module:ui/view~View} view A view to be removed from the balloon.
|
|
|
*/
|
|
|
@@ -157,43 +250,207 @@ export default class ContextualBalloon extends Plugin {
|
|
|
throw new CKEditorError( 'contextualballoon-remove-view-not-exist: Cannot remove configuration of not existing view.' );
|
|
|
}
|
|
|
|
|
|
- // When visible view is being removed.
|
|
|
- if ( this.visibleView === view ) {
|
|
|
- // We need to remove it from the view content.
|
|
|
- this.view.content.remove( view );
|
|
|
-
|
|
|
- // And then remove from the stack.
|
|
|
- this._stack.delete( view );
|
|
|
+ const stack = this._viewToStack.get( view );
|
|
|
|
|
|
- // Next we need to check if there is other view in stack to show.
|
|
|
- const last = Array.from( this._stack.values() ).pop();
|
|
|
+ if ( this._singleViewMode && this.visibleView === view ) {
|
|
|
+ this._singleViewMode = false;
|
|
|
+ }
|
|
|
|
|
|
- // If it is some other view.
|
|
|
- if ( last ) {
|
|
|
- // Just show it.
|
|
|
- this._show( last );
|
|
|
+ // When visible view will be removed we need to show a preceding view or next stack
|
|
|
+ // if a view is the only view in the stack.
|
|
|
+ if ( this.visibleView === view ) {
|
|
|
+ if ( stack.size === 1 ) {
|
|
|
+ if ( this._idToStack.size > 1 ) {
|
|
|
+ this._showNextStack();
|
|
|
+ } else {
|
|
|
+ this.view.hide();
|
|
|
+ this.visibleView = null;
|
|
|
+ this._rotatorView.hideView();
|
|
|
+ }
|
|
|
} else {
|
|
|
- // Hide the balloon panel.
|
|
|
- this.view.hide();
|
|
|
+ this._showView( Array.from( stack.values() )[ stack.size - 2 ] );
|
|
|
}
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( stack.size === 1 ) {
|
|
|
+ this._idToStack.delete( this._getStackId( stack ) );
|
|
|
+ this._numberOfStacks = this._idToStack.size;
|
|
|
} else {
|
|
|
- // Just remove given view from the stack.
|
|
|
- this._stack.delete( view );
|
|
|
+ stack.delete( view );
|
|
|
}
|
|
|
+
|
|
|
+ this._viewToStack.delete( view );
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Updates the position of the balloon using position data of the first visible view in the stack.
|
|
|
- * When new position data is given then position data of currently visible panel will be updated.
|
|
|
+ * When new position data is given then position data of currently visible view will be updated.
|
|
|
*
|
|
|
* @param {module:utils/dom/position~Options} [position] position options.
|
|
|
*/
|
|
|
updatePosition( position ) {
|
|
|
if ( position ) {
|
|
|
- this._stack.get( this.visibleView ).position = position;
|
|
|
+ this._visibleStack.get( this.visibleView ).position = position;
|
|
|
}
|
|
|
|
|
|
this.view.pin( this._getBalloonPosition() );
|
|
|
+ this._fakePanelsView.updatePosition();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shows last view from the stack of a given id.
|
|
|
+ *
|
|
|
+ * @param {String} id
|
|
|
+ */
|
|
|
+ showStack( id ) {
|
|
|
+ this.visibleStack = id;
|
|
|
+ const stack = this._idToStack.get( id );
|
|
|
+
|
|
|
+ if ( !stack ) {
|
|
|
+ /**
|
|
|
+ * Trying to show not existing stack.
|
|
|
+ *
|
|
|
+ * @error contextualballoon-showstack-stack-not-exist
|
|
|
+ */
|
|
|
+ throw new CKEditorError( 'contextualballoon-showstack-stack-not-exist: Cannot show not existing stack.' );
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( this._visibleStack === stack ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this._showView( Array.from( stack.values() ).pop() );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns the stack of currently visible view.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @type {Set}
|
|
|
+ */
|
|
|
+ get _visibleStack() {
|
|
|
+ return this._viewToStack.get( this.visibleView );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns id of given stack.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @param {Set} stack
|
|
|
+ * @returns {String}
|
|
|
+ */
|
|
|
+ _getStackId( stack ) {
|
|
|
+ const entry = Array.from( this._idToStack.entries() ).find( entry => entry[ 1 ] === stack );
|
|
|
+
|
|
|
+ return entry[ 0 ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shows last view from the next stack.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ _showNextStack() {
|
|
|
+ const stacks = Array.from( this._idToStack.values() );
|
|
|
+
|
|
|
+ let nextIndex = stacks.indexOf( this._visibleStack ) + 1;
|
|
|
+
|
|
|
+ if ( !stacks[ nextIndex ] ) {
|
|
|
+ nextIndex = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.showStack( this._getStackId( stacks[ nextIndex ] ) );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shows last view from the previous stack.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ */
|
|
|
+ _showPrevStack() {
|
|
|
+ const stacks = Array.from( this._idToStack.values() );
|
|
|
+
|
|
|
+ let nextIndex = stacks.indexOf( this._visibleStack ) - 1;
|
|
|
+
|
|
|
+ if ( !stacks[ nextIndex ] ) {
|
|
|
+ nextIndex = stacks.length - 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.showStack( this._getStackId( stacks[ nextIndex ] ) );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a rotator view.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @returns {module:ui/panel/balloon/contextualballoon~RotatorView}
|
|
|
+ */
|
|
|
+ _createRotatorView() {
|
|
|
+ const view = new RotatorView( this.editor.locale );
|
|
|
+ const t = this.editor.locale.t;
|
|
|
+
|
|
|
+ this.view.content.add( view );
|
|
|
+
|
|
|
+ // Hide navigation when there is only a one stack & not in single view mode.
|
|
|
+ view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( value, isSingleViewMode ) => {
|
|
|
+ return !isSingleViewMode && value > 1;
|
|
|
+ } );
|
|
|
+
|
|
|
+ // Update balloon position after toggling navigation.
|
|
|
+ view.on( 'change:isNavigationVisible', () => ( this.updatePosition() ), { priority: 'low' } );
|
|
|
+
|
|
|
+ // Update stacks counter value.
|
|
|
+ view.bind( 'counter' ).to( this, 'visibleView', this, '_numberOfStacks', ( visibleView, numberOfStacks ) => {
|
|
|
+ if ( numberOfStacks < 2 ) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ const current = Array.from( this._idToStack.values() ).indexOf( this._visibleStack ) + 1;
|
|
|
+
|
|
|
+ return t( '%0 of %1', [ current, numberOfStacks ] );
|
|
|
+ } );
|
|
|
+
|
|
|
+ view.buttonNextView.on( 'execute', () => {
|
|
|
+ // When current view has a focus then move focus to the editable before removing it,
|
|
|
+ // otherwise editor will lost focus.
|
|
|
+ if ( view.focusTracker.isFocused ) {
|
|
|
+ this.editor.editing.view.focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ this._showNextStack();
|
|
|
+ } );
|
|
|
+
|
|
|
+ view.buttonPrevView.on( 'execute', () => {
|
|
|
+ // When current view has a focus then move focus to the editable before removing it,
|
|
|
+ // otherwise editor will lost focus.
|
|
|
+ if ( view.focusTracker.isFocused ) {
|
|
|
+ this.editor.editing.view.focus();
|
|
|
+ }
|
|
|
+
|
|
|
+ this._showPrevStack();
|
|
|
+ } );
|
|
|
+
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @returns {module:ui/view~View}
|
|
|
+ */
|
|
|
+ _createFakePanelsView() {
|
|
|
+ const view = new FakePanelsView( this.editor.locale, this.view );
|
|
|
+
|
|
|
+ view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( number, isSingleViewMode ) => {
|
|
|
+ const showPanels = !isSingleViewMode && number >= 2;
|
|
|
+
|
|
|
+ return showPanels ? Math.min( number - 1, 2 ) : 0;
|
|
|
+ } );
|
|
|
+
|
|
|
+ view.listenTo( this.view, 'change:top', () => view.updatePosition() );
|
|
|
+ view.listenTo( this.view, 'change:left', () => view.updatePosition() );
|
|
|
+
|
|
|
+ this.editor.ui.view.body.add( view );
|
|
|
+
|
|
|
+ return view;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -206,12 +463,18 @@ export default class ContextualBalloon extends Plugin {
|
|
|
* @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.
|
|
|
*/
|
|
|
- _show( { view, balloonClassName = '', withArrow = true } ) {
|
|
|
+ _showView( { view, balloonClassName = '', withArrow = true, singleViewMode = false } ) {
|
|
|
this.view.class = balloonClassName;
|
|
|
this.view.withArrow = withArrow;
|
|
|
|
|
|
- this.view.content.add( view );
|
|
|
+ this._rotatorView.showView( view );
|
|
|
+ this.visibleView = view;
|
|
|
this.view.pin( this._getBalloonPosition() );
|
|
|
+ this._fakePanelsView.updatePosition();
|
|
|
+
|
|
|
+ if ( singleViewMode ) {
|
|
|
+ this._singleViewMode = true;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -222,7 +485,7 @@ export default class ContextualBalloon extends Plugin {
|
|
|
* @returns {module:utils/dom/position~Options}
|
|
|
*/
|
|
|
_getBalloonPosition() {
|
|
|
- let position = Array.from( this._stack.values() ).pop().position;
|
|
|
+ let position = Array.from( this._visibleStack.values() ).pop().position;
|
|
|
|
|
|
// Use the default limiter if none has been specified.
|
|
|
if ( position && !position.limiter ) {
|
|
|
@@ -235,3 +498,269 @@ export default class ContextualBalloon extends Plugin {
|
|
|
return position;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Rotator view is a helper class for the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon}.
|
|
|
+ * It is used for displaying last view from the current stack and providing navigation buttons for switching stacks.
|
|
|
+ * See {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon} documentation to learn more.
|
|
|
+ *
|
|
|
+ * @extends module:ui/view~View
|
|
|
+ */
|
|
|
+class RotatorView extends View {
|
|
|
+ /**
|
|
|
+ * @inheritDoc
|
|
|
+ */
|
|
|
+ constructor( locale ) {
|
|
|
+ super( locale );
|
|
|
+
|
|
|
+ const t = locale.t;
|
|
|
+ const bind = this.bindTemplate;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Defines whether navigation is visible or not.
|
|
|
+ *
|
|
|
+ * @member {Boolean} #isNavigationVisible
|
|
|
+ */
|
|
|
+ this.set( 'isNavigationVisible', true );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Used for checking if view is focused or not.
|
|
|
+ *
|
|
|
+ * @type {module:utils/focustracker~FocusTracker}
|
|
|
+ */
|
|
|
+ this.focusTracker = new FocusTracker();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Navigation button for switching stack to the previous one.
|
|
|
+ *
|
|
|
+ * @type {module:ui/button/buttonview~ButtonView}
|
|
|
+ */
|
|
|
+ this.buttonPrevView = this._createButtonView( t( 'Previous' ), prevIcon );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Navigation button for switching stack to the next one.
|
|
|
+ *
|
|
|
+ * @type {module:ui/button/buttonview~ButtonView}
|
|
|
+ */
|
|
|
+ this.buttonNextView = this._createButtonView( t( 'Next' ), nextIcon );
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Collection of the child views which creates rotator content.
|
|
|
+ *
|
|
|
+ * @readonly
|
|
|
+ * @type {module:ui/viewcollection~ViewCollection}
|
|
|
+ */
|
|
|
+ this.content = this.createCollection();
|
|
|
+
|
|
|
+ this.setTemplate( {
|
|
|
+ tag: 'div',
|
|
|
+ attributes: {
|
|
|
+ class: [
|
|
|
+ 'ck',
|
|
|
+ 'ck-balloon-rotator'
|
|
|
+ ],
|
|
|
+ 'z-index': '-1'
|
|
|
+ },
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ tag: 'div',
|
|
|
+ attributes: {
|
|
|
+ class: [
|
|
|
+ 'ck-balloon-rotator__navigation',
|
|
|
+ bind.to( 'isNavigationVisible', value => value ? '' : 'ck-hidden' )
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ children: [
|
|
|
+ this.buttonPrevView,
|
|
|
+ {
|
|
|
+ tag: 'span',
|
|
|
+
|
|
|
+ attributes: {
|
|
|
+ class: [
|
|
|
+ 'ck-balloon-rotator__counter'
|
|
|
+ ]
|
|
|
+ },
|
|
|
+
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ text: bind.to( 'counter' )
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ this.buttonNextView,
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ tag: 'div',
|
|
|
+ attributes: {
|
|
|
+ class: 'ck-balloon-rotator__content'
|
|
|
+ },
|
|
|
+ children: this.content
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ } );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @inheritDoc
|
|
|
+ */
|
|
|
+ render() {
|
|
|
+ super.render();
|
|
|
+
|
|
|
+ this.focusTracker.add( this.element );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Shows given view.
|
|
|
+ *
|
|
|
+ * @param {module:ui/view~View} view The view to show.
|
|
|
+ */
|
|
|
+ showView( view ) {
|
|
|
+ this.hideView();
|
|
|
+ this.content.add( view );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Hides currently displayed view.
|
|
|
+ */
|
|
|
+ hideView() {
|
|
|
+ this.content.clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Creates a navigation button view.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @param {String} label The button's label.
|
|
|
+ * @param {String} icon The button's icon.
|
|
|
+ * @returns {module:ui/button/buttonview~ButtonView}
|
|
|
+ */
|
|
|
+ _createButtonView( label, icon ) {
|
|
|
+ const view = new ButtonView( this.locale );
|
|
|
+
|
|
|
+ view.set( {
|
|
|
+ label,
|
|
|
+ icon,
|
|
|
+ tooltip: true
|
|
|
+ } );
|
|
|
+
|
|
|
+ return view;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Displays additional layers under the balloon when multiple stacks are added to the balloon.
|
|
|
+//
|
|
|
+// @private
|
|
|
+// @extends module:ui/view~View
|
|
|
+class FakePanelsView extends View {
|
|
|
+ // @inheritDoc
|
|
|
+ constructor( locale, balloonPanelView ) {
|
|
|
+ super( locale );
|
|
|
+
|
|
|
+ const bind = this.bindTemplate;
|
|
|
+
|
|
|
+ // Fake panels top offset.
|
|
|
+ //
|
|
|
+ // @observable
|
|
|
+ // @member {Number} #top
|
|
|
+ this.set( 'top', 0 );
|
|
|
+
|
|
|
+ // Fake panels left offset.
|
|
|
+ //
|
|
|
+ // @observable
|
|
|
+ // @member {Number} #left
|
|
|
+ this.set( 'left', 0 );
|
|
|
+
|
|
|
+ // Fake panels height.
|
|
|
+ //
|
|
|
+ // @observable
|
|
|
+ // @member {Number} #height
|
|
|
+ this.set( 'height', 0 );
|
|
|
+
|
|
|
+ // Fake panels width.
|
|
|
+ //
|
|
|
+ // @observable
|
|
|
+ // @member {Number} #width
|
|
|
+ this.set( 'width', 0 );
|
|
|
+
|
|
|
+ // Number of rendered fake panels.
|
|
|
+ //
|
|
|
+ // @observable
|
|
|
+ // @member {Number} #numberOfPanels
|
|
|
+ this.set( 'numberOfPanels', 0 );
|
|
|
+
|
|
|
+ // Collection of the child views which creates fake panel content.
|
|
|
+ //
|
|
|
+ // @readonly
|
|
|
+ // @type {module:ui/viewcollection~ViewCollection}
|
|
|
+ this.content = this.createCollection();
|
|
|
+
|
|
|
+ // Context.
|
|
|
+ //
|
|
|
+ // @private
|
|
|
+ // @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
|
|
|
+ this._balloonPanelView = balloonPanelView;
|
|
|
+
|
|
|
+ this.setTemplate( {
|
|
|
+ tag: 'div',
|
|
|
+ attributes: {
|
|
|
+ class: [
|
|
|
+ 'ck-fake-panel',
|
|
|
+ bind.to( 'numberOfPanels', number => number ? '' : 'ck-hidden' )
|
|
|
+ ],
|
|
|
+ style: {
|
|
|
+ top: bind.to( 'top', toPx ),
|
|
|
+ left: bind.to( 'left', toPx ),
|
|
|
+ width: bind.to( 'width', toPx ),
|
|
|
+ height: bind.to( 'height', toPx )
|
|
|
+ }
|
|
|
+ },
|
|
|
+ children: this.content
|
|
|
+ } );
|
|
|
+
|
|
|
+ this.on( 'change:numberOfPanels', ( evt, name, next, prev ) => {
|
|
|
+ if ( next > prev ) {
|
|
|
+ this._addPanels( next - prev );
|
|
|
+ } else {
|
|
|
+ this._removePanels( prev - next );
|
|
|
+ }
|
|
|
+
|
|
|
+ this.updatePosition();
|
|
|
+ } );
|
|
|
+ }
|
|
|
+
|
|
|
+ // @private
|
|
|
+ // @param {Number} number
|
|
|
+ _addPanels( number ) {
|
|
|
+ while ( number-- ) {
|
|
|
+ const view = new View();
|
|
|
+
|
|
|
+ view.setTemplate( { tag: 'div' } );
|
|
|
+
|
|
|
+ this.content.add( view );
|
|
|
+ this.registerChild( view );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // @private
|
|
|
+ // @param {Number} number
|
|
|
+ _removePanels( number ) {
|
|
|
+ while ( number-- ) {
|
|
|
+ const view = this.content.last;
|
|
|
+
|
|
|
+ this.content.remove( view );
|
|
|
+ this.deregisterChild( view );
|
|
|
+ view.destroy();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Updates coordinates of fake panels.
|
|
|
+ updatePosition() {
|
|
|
+ if ( this.numberOfPanels ) {
|
|
|
+ const { top, left } = this._balloonPanelView;
|
|
|
+ const { width, height } = new Rect( this._balloonPanelView.element );
|
|
|
+
|
|
|
+ Object.assign( this, { top, left, width, height } );
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|