contextualballoon.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module ui/panel/balloon/contextualballoon
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import BalloonPanelView from './balloonpanelview';
  10. import View from '../../view';
  11. import ButtonView from '../../button/buttonview';
  12. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  13. import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
  14. import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
  15. import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
  16. import prevIcon from '../../../theme/icons/previous-arrow.svg';
  17. import nextIcon from '../../../theme/icons/next-arrow.svg';
  18. import '../../../theme/components/panel/balloonrotator.css';
  19. import '../../../theme/components/panel/fakepanel.css';
  20. const toPx = toUnit( 'px' );
  21. /**
  22. * Provides the common contextual balloon for the editor.
  23. *
  24. * The role of this plugin is to unified contextual balloons logic, simplifies managing the views and helps
  25. * avoid the unnecessary complexity of handling multiple {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
  26. * instances in the editor.
  27. *
  28. * This plugin allows creating single or multiple panel stacks.
  29. *
  30. * Each stack may have multiple views, the one on the top is visible. When the visible view is removed from the stack,
  31. * the previous view becomes visible, etc. It might be useful to implement nested navigation in a balloon. For instance
  32. * toolbar view may have a link button. When you click it, link view (which let you set the URL) is created and put on
  33. * top of the toolbar view, so not link panel is displayed. When you finish editing link and close (remove) link view,
  34. * the toolbar view is visible back.
  35. *
  36. * However, there are cases when there are multiple independent balloons to be displayed. For instance, if the selection
  37. * is inside two inline comments at the same time. For such cases, you can create two independent panel stacks.
  38. * Contextual balloon plugin will create a navigation bar to let users switch between these panel stacks - "next balloon"
  39. * and "previous balloon" buttons.
  40. *
  41. * If there are no views in the current stack, the balloon panel will try to switch to the next stack. If there are no
  42. * panels in any stack then balloon panel will be hidden.
  43. *
  44. * **Note**: To force balloon panel to show only one view - even if there are other stacks - use `singleViewMode=true` option
  45. * when {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#add adding} view to a panel.
  46. *
  47. * From the implementation point of view, contextual ballon plugin is reusing a single
  48. * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance to display multiple contextual balloon
  49. * panels in the editor. It also creates a special {@link module:ui/panel/balloon/contextualballoon~RotatorView rotator view},
  50. * used to manage multiple panel stacks. Rotator view is a child of the balloon panel view and the parent of the specific
  51. * view you want to display. If there is are more than one panel stack to be displayed, the rotator view will add a
  52. * navigation bar. If there is only one stack, rotator view is transparent (do not add any UI elements).
  53. *
  54. * @extends module:core/plugin~Plugin
  55. */
  56. export default class ContextualBalloon extends Plugin {
  57. /**
  58. * @inheritDoc
  59. */
  60. static get pluginName() {
  61. return 'ContextualBalloon';
  62. }
  63. /**
  64. * @inheritDoc
  65. */
  66. constructor( editor ) {
  67. super( editor );
  68. /**
  69. * The {@link module:utils/dom/position~Options#limiter position limiter}
  70. * for the {@link #view balloon}, used when no `limiter` has been passed into {@link #add}
  71. * or {@link #updatePosition}.
  72. *
  73. * By default, a function, which obtains the farthest DOM
  74. * {@link module:engine/view/rooteditableelement~RootEditableElement}
  75. * of the {@link module:engine/view/document~Document#selection}.
  76. *
  77. * @member {module:utils/dom/position~Options#limiter} #positionLimiter
  78. */
  79. this.positionLimiter = () => {
  80. const view = this.editor.editing.view;
  81. const viewDocument = view.document;
  82. const editableElement = viewDocument.selection.editableElement;
  83. if ( editableElement ) {
  84. return view.domConverter.mapViewToDom( editableElement.root );
  85. }
  86. return null;
  87. };
  88. /**
  89. * The currently visible view or `null` when there are no views in the any stack.
  90. *
  91. * @readonly
  92. * @observable
  93. * @member {module:ui/view~View|null} #visibleView
  94. */
  95. this.set( 'visibleView', null );
  96. /**
  97. * The common balloon panel view.
  98. *
  99. * @readonly
  100. * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
  101. */
  102. this.view = new BalloonPanelView( editor.locale );
  103. editor.ui.view.body.add( this.view );
  104. editor.ui.focusTracker.add( this.view.element );
  105. /**
  106. * Map of views and its stacks.
  107. *
  108. * @private
  109. * @type {Map.<module:ui/view~View,Set>}
  110. */
  111. this._viewToStack = new Map();
  112. /**
  113. * Map of ids and stacks.
  114. *
  115. * @private
  116. * @type {Map.<String,Set>}
  117. */
  118. this._idToStack = new Map();
  119. /**
  120. * A total number of all stacks in the balloon.
  121. *
  122. * @private
  123. * @readonly
  124. * @observable
  125. * @member {Number} #_numberOfStacks
  126. */
  127. this.set( '_numberOfStacks', 0 );
  128. /**
  129. * Flag that controls the single view mode.
  130. *
  131. * @private
  132. * @readonly
  133. * @observable
  134. * @member {Boolean} #_singleViewMode
  135. */
  136. this.set( '_singleViewMode', false );
  137. /**
  138. * Rotator view embedded in the contextual balloon.
  139. * Displays currently visible view in the balloon and provides navigation for switching stacks.
  140. *
  141. * @private
  142. * @type {module:ui/panel/balloon/contextualballoon~RotatorView}
  143. */
  144. this._rotatorView = this._createRotatorView();
  145. /**
  146. * Displays fake panels under the balloon panel view when multiple stacks are added to the balloon.
  147. *
  148. * @private
  149. * @type {module:ui/view~View}
  150. */
  151. this._fakePanelsView = this._createFakePanelsView();
  152. }
  153. /**
  154. * Returns `true` when the given view is in one of the stack. Otherwise returns `false`.
  155. *
  156. * @param {module:ui/view~View} view
  157. * @returns {Boolean}
  158. */
  159. hasView( view ) {
  160. return Array.from( this._viewToStack.keys() ).includes( view );
  161. }
  162. /**
  163. * Adds a new view to the stack and makes it visible if current stack is visible
  164. * or it is a first view in the balloon.
  165. *
  166. * @param {Object} data Configuration of the view.
  167. * @param {String} [data.stackId='main'] Id of a stack that view is added to.
  168. * @param {module:ui/view~View} [data.view] Content of the balloon.
  169. * @param {module:utils/dom/position~Options} [data.position] Positioning options.
  170. * @param {String} [data.balloonClassName] Additional CSS class added to the {@link #view balloon} when visible.
  171. * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
  172. * @param {Boolean} [data.singleViewMode=false] Whether the view should be only visible view - even if other stacks were added.
  173. */
  174. add( data ) {
  175. if ( this.hasView( data.view ) ) {
  176. /**
  177. * Trying to add configuration of the same view more than once.
  178. *
  179. * @error contextualballoon-add-view-exist
  180. */
  181. throw new CKEditorError( 'contextualballoon-add-view-exist: Cannot add configuration of the same view twice.' );
  182. }
  183. const stackId = data.stackId || 'main';
  184. // If new stack is added, creates it and show view from this stack.
  185. if ( !this._idToStack.has( stackId ) ) {
  186. this._idToStack.set( stackId, new Map( [ [ data.view, data ] ] ) );
  187. this._viewToStack.set( data.view, this._idToStack.get( stackId ) );
  188. this._numberOfStacks = this._idToStack.size;
  189. if ( !this._visibleStack || data.singleViewMode ) {
  190. this.showStack( stackId );
  191. }
  192. return;
  193. }
  194. const stack = this._idToStack.get( stackId );
  195. if ( data.singleViewMode ) {
  196. this.showStack( stackId );
  197. }
  198. // Add new view to the stack.
  199. stack.set( data.view, data );
  200. this._viewToStack.set( data.view, stack );
  201. // And display it if is added to the currently visible stack.
  202. if ( stack === this._visibleStack ) {
  203. this._showView( data );
  204. }
  205. }
  206. /**
  207. * Removes the given view from the stack. If the removed view was visible,
  208. * then the view preceding it in the stack will become visible instead.
  209. * When there is no view in the stack then next stack will be displayed.
  210. * When there is not more stacks then balloon will hide.
  211. *
  212. * @param {module:ui/view~View} view A view to be removed from the balloon.
  213. */
  214. remove( view ) {
  215. if ( !this.hasView( view ) ) {
  216. /**
  217. * Trying to remove configuration of the view not defined in the stack.
  218. *
  219. * @error contextualballoon-remove-view-not-exist
  220. */
  221. throw new CKEditorError( 'contextualballoon-remove-view-not-exist: Cannot remove configuration of not existing view.' );
  222. }
  223. const stack = this._viewToStack.get( view );
  224. if ( this._singleViewMode && this.visibleView === view ) {
  225. this._singleViewMode = false;
  226. }
  227. // When visible view will be removed we need to show a preceding view or next stack
  228. // if a view is the only view in the stack.
  229. if ( this.visibleView === view ) {
  230. if ( stack.size === 1 ) {
  231. if ( this._idToStack.size > 1 ) {
  232. this._showNextStack();
  233. } else {
  234. this.view.hide();
  235. this.visibleView = null;
  236. this._rotatorView.hideView();
  237. }
  238. } else {
  239. this._showView( Array.from( stack.values() )[ stack.size - 2 ] );
  240. }
  241. }
  242. if ( stack.size === 1 ) {
  243. this._idToStack.delete( this._getStackId( stack ) );
  244. this._numberOfStacks = this._idToStack.size;
  245. } else {
  246. stack.delete( view );
  247. }
  248. this._viewToStack.delete( view );
  249. }
  250. /**
  251. * Updates the position of the balloon using position data of the first visible view in the stack.
  252. * When new position data is given then position data of currently visible view will be updated.
  253. *
  254. * @param {module:utils/dom/position~Options} [position] position options.
  255. */
  256. updatePosition( position ) {
  257. if ( position ) {
  258. this._visibleStack.get( this.visibleView ).position = position;
  259. }
  260. this.view.pin( this._getBalloonPosition() );
  261. this._fakePanelsView.updatePosition();
  262. }
  263. /**
  264. * Shows last view from the stack of a given id.
  265. *
  266. * @param {String} id
  267. */
  268. showStack( id ) {
  269. this.visibleStack = id;
  270. const stack = this._idToStack.get( id );
  271. if ( !stack ) {
  272. /**
  273. * Trying to show not existing stack.
  274. *
  275. * @error contextualballoon-showstack-stack-not-exist
  276. */
  277. throw new CKEditorError( 'contextualballoon-showstack-stack-not-exist: Cannot show not existing stack.' );
  278. }
  279. if ( this._visibleStack === stack ) {
  280. return;
  281. }
  282. this._showView( Array.from( stack.values() ).pop() );
  283. }
  284. /**
  285. * Returns the stack of currently visible view.
  286. *
  287. * @private
  288. * @type {Set}
  289. */
  290. get _visibleStack() {
  291. return this._viewToStack.get( this.visibleView );
  292. }
  293. /**
  294. * Returns id of given stack.
  295. *
  296. * @private
  297. * @param {Set} stack
  298. * @returns {String}
  299. */
  300. _getStackId( stack ) {
  301. const entry = Array.from( this._idToStack.entries() ).find( entry => entry[ 1 ] === stack );
  302. return entry[ 0 ];
  303. }
  304. /**
  305. * Shows last view from the next stack.
  306. *
  307. * @private
  308. */
  309. _showNextStack() {
  310. const stacks = Array.from( this._idToStack.values() );
  311. let nextIndex = stacks.indexOf( this._visibleStack ) + 1;
  312. if ( !stacks[ nextIndex ] ) {
  313. nextIndex = 0;
  314. }
  315. this.showStack( this._getStackId( stacks[ nextIndex ] ) );
  316. }
  317. /**
  318. * Shows last view from the previous stack.
  319. *
  320. * @private
  321. */
  322. _showPrevStack() {
  323. const stacks = Array.from( this._idToStack.values() );
  324. let nextIndex = stacks.indexOf( this._visibleStack ) - 1;
  325. if ( !stacks[ nextIndex ] ) {
  326. nextIndex = stacks.length - 1;
  327. }
  328. this.showStack( this._getStackId( stacks[ nextIndex ] ) );
  329. }
  330. /**
  331. * Creates a rotator view.
  332. *
  333. * @private
  334. * @returns {module:ui/panel/balloon/contextualballoon~RotatorView}
  335. */
  336. _createRotatorView() {
  337. const view = new RotatorView( this.editor.locale );
  338. const t = this.editor.locale.t;
  339. this.view.content.add( view );
  340. // Hide navigation when there is only a one stack & not in single view mode.
  341. view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( value, isSingleViewMode ) => {
  342. return !isSingleViewMode && value > 1;
  343. } );
  344. // Update balloon position after toggling navigation.
  345. view.on( 'change:isNavigationVisible', () => ( this.updatePosition() ), { priority: 'low' } );
  346. // Update stacks counter value.
  347. view.bind( 'counter' ).to( this, 'visibleView', this, '_numberOfStacks', ( visibleView, numberOfStacks ) => {
  348. if ( numberOfStacks < 2 ) {
  349. return '';
  350. }
  351. const current = Array.from( this._idToStack.values() ).indexOf( this._visibleStack ) + 1;
  352. return t( '%0 of %1', [ current, numberOfStacks ] );
  353. } );
  354. view.buttonNextView.on( 'execute', () => {
  355. // When current view has a focus then move focus to the editable before removing it,
  356. // otherwise editor will lost focus.
  357. if ( view.focusTracker.isFocused ) {
  358. this.editor.editing.view.focus();
  359. }
  360. this._showNextStack();
  361. } );
  362. view.buttonPrevView.on( 'execute', () => {
  363. // When current view has a focus then move focus to the editable before removing it,
  364. // otherwise editor will lost focus.
  365. if ( view.focusTracker.isFocused ) {
  366. this.editor.editing.view.focus();
  367. }
  368. this._showPrevStack();
  369. } );
  370. return view;
  371. }
  372. /**
  373. * @returns {module:ui/view~View}
  374. */
  375. _createFakePanelsView() {
  376. const view = new FakePanelsView( this.editor.locale, this.view );
  377. view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( number, isSingleViewMode ) => {
  378. const showPanels = !isSingleViewMode && number >= 2;
  379. return showPanels ? Math.min( number - 1, 2 ) : 0;
  380. } );
  381. view.listenTo( this.view, 'change:top', () => view.updatePosition() );
  382. view.listenTo( this.view, 'change:left', () => view.updatePosition() );
  383. this.editor.ui.view.body.add( view );
  384. return view;
  385. }
  386. /**
  387. * Sets the view as a content of the balloon and attaches balloon using position
  388. * options of the first view.
  389. *
  390. * @private
  391. * @param {Object} data Configuration.
  392. * @param {module:ui/view~View} [data.view] View to show in the balloon.
  393. * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
  394. * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
  395. */
  396. _showView( { view, balloonClassName = '', withArrow = true, singleViewMode = false } ) {
  397. this.view.class = balloonClassName;
  398. this.view.withArrow = withArrow;
  399. this._rotatorView.showView( view );
  400. this.visibleView = view;
  401. this.view.pin( this._getBalloonPosition() );
  402. this._fakePanelsView.updatePosition();
  403. if ( singleViewMode ) {
  404. this._singleViewMode = true;
  405. }
  406. }
  407. /**
  408. * Returns position options of the last view in the stack.
  409. * This keeps the balloon in the same position when view is changed.
  410. *
  411. * @private
  412. * @returns {module:utils/dom/position~Options}
  413. */
  414. _getBalloonPosition() {
  415. let position = Array.from( this._visibleStack.values() ).pop().position;
  416. // Use the default limiter if none has been specified.
  417. if ( position && !position.limiter ) {
  418. // Don't modify the original options object.
  419. position = Object.assign( {}, position, {
  420. limiter: this.positionLimiter
  421. } );
  422. }
  423. return position;
  424. }
  425. }
  426. /**
  427. * Rotator view is a helper class for the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon}.
  428. * It is used for displaying last view from the current stack and providing navigation buttons for switching stacks.
  429. * See {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon ContextualBalloon} documentation to learn more.
  430. *
  431. * @extends module:ui/view~View
  432. */
  433. class RotatorView extends View {
  434. /**
  435. * @inheritDoc
  436. */
  437. constructor( locale ) {
  438. super( locale );
  439. const t = locale.t;
  440. const bind = this.bindTemplate;
  441. /**
  442. * Defines whether navigation is visible or not.
  443. *
  444. * @member {Boolean} #isNavigationVisible
  445. */
  446. this.set( 'isNavigationVisible', true );
  447. /**
  448. * Used for checking if view is focused or not.
  449. *
  450. * @type {module:utils/focustracker~FocusTracker}
  451. */
  452. this.focusTracker = new FocusTracker();
  453. /**
  454. * Navigation button for switching stack to the previous one.
  455. *
  456. * @type {module:ui/button/buttonview~ButtonView}
  457. */
  458. this.buttonPrevView = this._createButtonView( t( 'Previous' ), prevIcon );
  459. /**
  460. * Navigation button for switching stack to the next one.
  461. *
  462. * @type {module:ui/button/buttonview~ButtonView}
  463. */
  464. this.buttonNextView = this._createButtonView( t( 'Next' ), nextIcon );
  465. /**
  466. * Collection of the child views which creates rotator content.
  467. *
  468. * @readonly
  469. * @type {module:ui/viewcollection~ViewCollection}
  470. */
  471. this.content = this.createCollection();
  472. this.setTemplate( {
  473. tag: 'div',
  474. attributes: {
  475. class: [
  476. 'ck',
  477. 'ck-balloon-rotator'
  478. ],
  479. 'z-index': '-1'
  480. },
  481. children: [
  482. {
  483. tag: 'div',
  484. attributes: {
  485. class: [
  486. 'ck-balloon-rotator__navigation',
  487. bind.to( 'isNavigationVisible', value => value ? '' : 'ck-hidden' )
  488. ]
  489. },
  490. children: [
  491. this.buttonPrevView,
  492. {
  493. tag: 'span',
  494. attributes: {
  495. class: [
  496. 'ck-balloon-rotator__counter'
  497. ]
  498. },
  499. children: [
  500. {
  501. text: bind.to( 'counter' )
  502. }
  503. ]
  504. },
  505. this.buttonNextView,
  506. ]
  507. },
  508. {
  509. tag: 'div',
  510. attributes: {
  511. class: 'ck-balloon-rotator__content'
  512. },
  513. children: this.content
  514. }
  515. ]
  516. } );
  517. }
  518. /**
  519. * @inheritDoc
  520. */
  521. render() {
  522. super.render();
  523. this.focusTracker.add( this.element );
  524. }
  525. /**
  526. * Shows given view.
  527. *
  528. * @param {module:ui/view~View} view The view to show.
  529. */
  530. showView( view ) {
  531. this.hideView();
  532. this.content.add( view );
  533. }
  534. /**
  535. * Hides currently displayed view.
  536. */
  537. hideView() {
  538. this.content.clear();
  539. }
  540. /**
  541. * Creates a navigation button view.
  542. *
  543. * @private
  544. * @param {String} label The button's label.
  545. * @param {String} icon The button's icon.
  546. * @returns {module:ui/button/buttonview~ButtonView}
  547. */
  548. _createButtonView( label, icon ) {
  549. const view = new ButtonView( this.locale );
  550. view.set( {
  551. label,
  552. icon,
  553. tooltip: true
  554. } );
  555. return view;
  556. }
  557. }
  558. // Displays additional layers under the balloon when multiple stacks are added to the balloon.
  559. //
  560. // @private
  561. // @extends module:ui/view~View
  562. class FakePanelsView extends View {
  563. // @inheritDoc
  564. constructor( locale, balloonPanelView ) {
  565. super( locale );
  566. const bind = this.bindTemplate;
  567. // Fake panels top offset.
  568. //
  569. // @observable
  570. // @member {Number} #top
  571. this.set( 'top', 0 );
  572. // Fake panels left offset.
  573. //
  574. // @observable
  575. // @member {Number} #left
  576. this.set( 'left', 0 );
  577. // Fake panels height.
  578. //
  579. // @observable
  580. // @member {Number} #height
  581. this.set( 'height', 0 );
  582. // Fake panels width.
  583. //
  584. // @observable
  585. // @member {Number} #width
  586. this.set( 'width', 0 );
  587. // Number of rendered fake panels.
  588. //
  589. // @observable
  590. // @member {Number} #numberOfPanels
  591. this.set( 'numberOfPanels', 0 );
  592. // Collection of the child views which creates fake panel content.
  593. //
  594. // @readonly
  595. // @type {module:ui/viewcollection~ViewCollection}
  596. this.content = this.createCollection();
  597. // Context.
  598. //
  599. // @private
  600. // @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
  601. this._balloonPanelView = balloonPanelView;
  602. this.setTemplate( {
  603. tag: 'div',
  604. attributes: {
  605. class: [
  606. 'ck-fake-panel',
  607. bind.to( 'numberOfPanels', number => number ? '' : 'ck-hidden' )
  608. ],
  609. style: {
  610. top: bind.to( 'top', toPx ),
  611. left: bind.to( 'left', toPx ),
  612. width: bind.to( 'width', toPx ),
  613. height: bind.to( 'height', toPx )
  614. }
  615. },
  616. children: this.content
  617. } );
  618. this.on( 'change:numberOfPanels', ( evt, name, next, prev ) => {
  619. if ( next > prev ) {
  620. this._addPanels( next - prev );
  621. } else {
  622. this._removePanels( prev - next );
  623. }
  624. this.updatePosition();
  625. } );
  626. }
  627. // @private
  628. // @param {Number} number
  629. _addPanels( number ) {
  630. while ( number-- ) {
  631. const view = new View();
  632. view.setTemplate( { tag: 'div' } );
  633. this.content.add( view );
  634. this.registerChild( view );
  635. }
  636. }
  637. // @private
  638. // @param {Number} number
  639. _removePanels( number ) {
  640. while ( number-- ) {
  641. const view = this.content.last;
  642. this.content.remove( view );
  643. this.deregisterChild( view );
  644. view.destroy();
  645. }
  646. }
  647. // Updates coordinates of fake panels.
  648. updatePosition() {
  649. if ( this.numberOfPanels ) {
  650. const { top, left } = this._balloonPanelView;
  651. const { width, height } = new Rect( this._balloonPanelView.element );
  652. Object.assign( this, { top, left, width, height } );
  653. }
  654. }
  655. }