contextualballoon.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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 '@ckeditor/ckeditor5-ui/src/view';
  11. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  12. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  13. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  14. /**
  15. * Provides the common contextual balloon panel for the editor.
  16. *
  17. * This plugin allows reusing a single {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance
  18. * to display multiple contextual balloon panels in the editor.
  19. *
  20. * Child views of such a panel are stored in the stack and the last one in the stack is visible. When the
  21. * visible view is removed from the stack, the previous view becomes visible, etc. If there are no more
  22. * views in the stack, the balloon panel will hide.
  23. *
  24. * It 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. * @extends module:core/plugin~Plugin
  29. */
  30. export default class ContextualBalloon extends Plugin {
  31. /**
  32. * @inheritDoc
  33. */
  34. static get pluginName() {
  35. return 'ContextualBalloon';
  36. }
  37. /**
  38. * @inheritDoc
  39. */
  40. constructor( editor ) {
  41. super( editor );
  42. /**
  43. * The {@link module:utils/dom/position~Options#limiter position limiter}
  44. * for the {@link #view balloon}, used when no `limiter` has been passed into {@link #add}
  45. * or {@link #updatePosition}.
  46. *
  47. * By default, a function, which obtains the farthest DOM
  48. * {@link module:engine/view/rooteditableelement~RootEditableElement}
  49. * of the {@link module:engine/view/document~Document#selection}.
  50. *
  51. * @member {module:utils/dom/position~Options#limiter} #positionLimiter
  52. */
  53. this.positionLimiter = () => {
  54. const view = this.editor.editing.view;
  55. const viewDocument = view.document;
  56. const editableElement = viewDocument.selection.editableElement;
  57. if ( editableElement ) {
  58. return view.domConverter.mapViewToDom( editableElement.root );
  59. }
  60. return null;
  61. };
  62. /**
  63. * The currently visible view or `null` when there are no
  64. * views in the currently visible panel stack.
  65. *
  66. * @readonly
  67. * @observable
  68. * @member {module:ui/view~View|null} #visibleView
  69. */
  70. this.set( 'visibleView', null );
  71. /**
  72. * The common balloon panel view.
  73. *
  74. * @readonly
  75. * @member {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} #view
  76. */
  77. this.view = new BalloonPanelView( editor.locale );
  78. this.editor.ui.view.body.add( this.view );
  79. this.editor.ui.focusTracker.add( this.view.element );
  80. /**
  81. * Currently visible panel.
  82. *
  83. * @private
  84. * @readonly
  85. * @observable
  86. * @member {null|Object} #_visiblePanel
  87. */
  88. this.set( '_visiblePanel', null );
  89. /**
  90. * List of panels.
  91. *
  92. * @private
  93. * @type {module:utils/collection~Collection}
  94. */
  95. this._panels = new Collection();
  96. /**
  97. * @private
  98. * @type {Map.<Object>}
  99. */
  100. this._viewToPanel = new Map();
  101. /**
  102. * Rotator view embedded in the contextual balloon.
  103. * Displays currently visible view in the balloon and provides navigation for switching panels.
  104. *
  105. * @private
  106. * @type {module:ui/view~View}
  107. */
  108. this._rotatorView = this._createRotatorView();
  109. }
  110. /**
  111. * Returns `true` when the given view is in one of the stack. Otherwise returns `false`.
  112. *
  113. * @param {module:ui/view~View} view
  114. * @returns {Boolean}
  115. */
  116. hasView( view ) {
  117. return Array.from( this._viewToPanel.keys() ).includes( view );
  118. }
  119. /**
  120. * Adds a new view to the panel stack and makes it visible if current panel is visible.
  121. *
  122. * @param {Object} data Configuration of the view.
  123. * @param {String} [data.panelId='main'] Id of panel that view is added to.
  124. * @param {module:ui/view~View} [data.view] Content of the balloon.
  125. * @param {module:utils/dom/position~Options} [data.position] Positioning options.
  126. * @param {String} [data.balloonClassName] Additional CSS class added to the {@link #view balloon} when visible.
  127. * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
  128. */
  129. add( data ) {
  130. if ( this.hasView( data.view ) ) {
  131. /**
  132. * Trying to add configuration of the same view more than once.
  133. *
  134. * @error contextualballoon-add-view-exist
  135. */
  136. throw new CKEditorError( 'contextualballoon-add-view-exist: Cannot add configuration of the same view twice.' );
  137. }
  138. const panelId = data.panelId || 'main';
  139. // If new panel is added, creates it and show it.
  140. if ( !this._panels.has( panelId ) ) {
  141. this._panels.add( {
  142. id: panelId,
  143. stack: new Map( [ [ data.view, data ] ] )
  144. } );
  145. this._viewToPanel.set( data.view, this._panels.get( panelId ) );
  146. if ( !this._visiblePanel ) {
  147. this.showPanel( panelId );
  148. }
  149. return;
  150. }
  151. const panel = this._panels.get( panelId );
  152. // Add new view to the stack.
  153. panel.stack.set( data.view, data );
  154. this._viewToPanel.set( data.view, panel );
  155. // And display it if is added to the currently visible panel.
  156. if ( panel === this._visiblePanel ) {
  157. this._showView( data );
  158. }
  159. }
  160. /**
  161. * Removes the given view from the panel stack. If the removed view was visible,
  162. * then the view preceding it in panel the stack will become visible instead.
  163. * When there is no view in the stack then next panel will be displayed.
  164. * When there is not panel to display then balloon will hide.
  165. *
  166. * @param {module:ui/view~View} view A view to be removed from the balloon.
  167. */
  168. remove( view ) {
  169. if ( !this.hasView( view ) ) {
  170. /**
  171. * Trying to remove configuration of the view not defined in the stack.
  172. *
  173. * @error contextualballoon-remove-view-not-exist
  174. */
  175. throw new CKEditorError( 'contextualballoon-remove-view-not-exist: Cannot remove configuration of not existing view.' );
  176. }
  177. const panel = this._viewToPanel.get( view );
  178. // When visible view will be removed we need to show a preceding view or next panel
  179. // if a view is the only view in the panel.
  180. if ( this.visibleView === view ) {
  181. if ( panel.stack.size === 1 ) {
  182. if ( this._panels.length > 1 ) {
  183. this._showNextPanel();
  184. } else {
  185. this.view.unpin();
  186. this.visibleView = null;
  187. this._visiblePanel = null;
  188. this._rotatorView.content.clear();
  189. }
  190. } else {
  191. this._showView( Array.from( panel.stack.values() )[ panel.stack.size - 2 ] );
  192. }
  193. }
  194. if ( panel.stack.size === 1 ) {
  195. this._panels.remove( panel.id );
  196. } else {
  197. panel.stack.delete( view );
  198. }
  199. this._viewToPanel.delete( view );
  200. }
  201. /**
  202. * Updates the position of the balloon using position data of the first visible view in the panel stack.
  203. * When new position data is given then position data of currently visible panel will be updated.
  204. *
  205. * @param {module:utils/dom/position~Options} [position] position options.
  206. */
  207. updatePosition( position ) {
  208. if ( position ) {
  209. this._visiblePanel.stack.get( this.visibleView ).position = position;
  210. }
  211. this.view.pin( this._getBalloonPosition() );
  212. }
  213. /**
  214. * Shows last view from the stack of panel with given id.
  215. *
  216. * @param {String} id
  217. */
  218. showPanel( id ) {
  219. const panel = this._panels.get( id );
  220. if ( !panel ) {
  221. /**
  222. * Trying to show not existing panel.
  223. *
  224. * @error contextualballoon-showpanel-panel-not-exist
  225. */
  226. throw new CKEditorError( 'contextualballoon-showpanel-panel-not-exist: Cannot show not existing panel.' );
  227. }
  228. if ( this._visiblePanel === panel ) {
  229. return;
  230. }
  231. this._visiblePanel = panel;
  232. this._showView( Array.from( panel.stack.values() ).pop() );
  233. }
  234. /**
  235. * Shows last view from the stack of the next panel
  236. * according to the currently visible panel.
  237. *
  238. * @private
  239. */
  240. _showNextPanel() {
  241. if ( this._panels.length === 1 ) {
  242. return;
  243. }
  244. const panel = this._visiblePanel;
  245. let nextIndex = this._panels.getIndex( panel ) + 1;
  246. if ( !this._panels.get( nextIndex ) ) {
  247. nextIndex = 0;
  248. }
  249. this.showPanel( this._panels.get( nextIndex ).id );
  250. }
  251. /**
  252. * Shows last view from the stack of the previous panel
  253. * according to the currently visible panel.
  254. *
  255. * @private
  256. */
  257. _showPrevPanel() {
  258. if ( this._panels.length === 1 ) {
  259. return;
  260. }
  261. const panel = this._visiblePanel;
  262. let nextIndex = this._panels.getIndex( panel ) - 1;
  263. if ( !this._panels.get( nextIndex ) ) {
  264. nextIndex = this._panels.length - 1;
  265. }
  266. this.showPanel( this._panels.get( nextIndex ).id );
  267. }
  268. /**
  269. * Creates a rotator view.
  270. *
  271. * @private
  272. * @returns {~RotatorView}
  273. */
  274. _createRotatorView() {
  275. const view = new RotatorView( this.editor.locale );
  276. this.view.content.add( view );
  277. // Hide navigation when there is only a one panel.
  278. view.bind( 'isNavigationVisible' ).to( this._panels, 'length', value => value > 1 );
  279. // Show panels counter.
  280. view.bind( 'counter' ).to( this, '_visiblePanel', this._panels, 'length', ( panel, length ) => {
  281. if ( !panel ) {
  282. return `0/${ length }`;
  283. }
  284. return `${ this._panels.getIndex( panel ) + 1 }/${ length }`;
  285. } );
  286. view.buttonNextView.on( 'execute', () => this._showNextPanel() );
  287. view.buttonPrevView.on( 'execute', () => this._showPrevPanel() );
  288. return view;
  289. }
  290. /**
  291. * Sets the view as a content of the balloon and attaches balloon using position
  292. * options of the first view.
  293. *
  294. * @private
  295. * @param {Object} data Configuration.
  296. * @param {module:ui/view~View} [data.view] View to show in the balloon.
  297. * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
  298. * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
  299. */
  300. _showView( { view, balloonClassName = '', withArrow = true } ) {
  301. this.view.class = balloonClassName;
  302. this.view.withArrow = withArrow;
  303. this._rotatorView.content.clear();
  304. this._rotatorView.content.add( view );
  305. this.visibleView = view;
  306. this.view.pin( this._getBalloonPosition() );
  307. }
  308. /**
  309. * Returns position options of the last view in the stack.
  310. * This keeps the balloon in the same position when view is changed.
  311. *
  312. * @private
  313. * @returns {module:utils/dom/position~Options}
  314. */
  315. _getBalloonPosition() {
  316. let position = Array.from( this._visiblePanel.stack.values() ).pop().position;
  317. // Use the default limiter if none has been specified.
  318. if ( position && !position.limiter ) {
  319. // Don't modify the original options object.
  320. position = Object.assign( {}, position, {
  321. limiter: this.positionLimiter
  322. } );
  323. }
  324. return position;
  325. }
  326. }
  327. // Rotator view. Used to display last view from the panel stack.
  328. // Provides navigation buttons for switching panels.
  329. //
  330. // @private
  331. // @extends module:ui/view~View
  332. class RotatorView extends View {
  333. constructor( locale ) {
  334. super( locale );
  335. const bind = this.bindTemplate;
  336. // Defines whether navigation is visible or not.
  337. //
  338. // @member {Boolean} #isNavigationVisible
  339. this.set( 'isNavigationVisible', true );
  340. // Navigation button to switches panel to the next one.
  341. //
  342. // @type {module:ui/button/buttonview~ButtonView}
  343. this.buttonPrevView = this._createButtonView( locale.t( 'Prev' ) );
  344. // Navigation button to switches panel to the previous one.
  345. //
  346. // @type {module:ui/button/buttonview~ButtonView}
  347. this.buttonNextView = this._createButtonView( locale.t( 'Next' ) );
  348. // Collection of the child views which creates balloon panel contents.
  349. //
  350. // @readonly
  351. // @type {module:ui/viewcollection~ViewCollection}
  352. this.content = this.createCollection();
  353. this.setTemplate( {
  354. tag: 'div',
  355. attributes: {
  356. class: 'rotator'
  357. },
  358. children: [
  359. {
  360. tag: 'div',
  361. attributes: {
  362. class: [
  363. 'rotator_navigation',
  364. bind.to( 'isNavigationVisible', value => value ? '' : 'ck-hidden' )
  365. ]
  366. },
  367. children: [
  368. this.buttonPrevView,
  369. this.buttonNextView,
  370. {
  371. text: bind.to( 'counter' )
  372. }
  373. ]
  374. },
  375. {
  376. tag: 'div',
  377. attributes: {
  378. class: 'rotator_content'
  379. },
  380. children: this.content
  381. }
  382. ]
  383. } );
  384. }
  385. // Create navigation button view.
  386. //
  387. // @private
  388. // @param {String} label
  389. // @returns {module:ui/button/buttonview~ButtonView}
  390. _createButtonView( label ) {
  391. const view = new ButtonView( this.locale );
  392. view.set( {
  393. withText: true,
  394. label
  395. } );
  396. return view;
  397. }
  398. }