widgettoolbarrepository.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module widget/widgettoolbarrepository
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
  10. import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
  11. import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
  12. import { isWidget } from './utils';
  13. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  14. /**
  15. * Widget toolbar repository plugin. A central point for creating widget toolbars. This plugin handles the whole
  16. * toolbar rendering process and exposes concise API.
  17. *
  18. * Creating toolbar for the widget bases on the {@link ~WidgetToolbarRepository#register} method.
  19. *
  20. * This plugin adds to the plugin list directly or indirectly prevents showing up
  21. * the {@link module:ui/toolbar/balloon/balloontoolbar~BalloonToolbar} toolbar and the widget toolbar at the same time.
  22. *
  23. * Usage example comes from {@link module:image/imagetoolbar~ImageToolbar}:
  24. *
  25. * class ImageToolbar extends Plugin {
  26. * static get requires() {
  27. * return [ WidgetToolbarRepository ];
  28. * }
  29. *
  30. * afterInit() {
  31. * const editor = this.editor;
  32. * const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
  33. *
  34. * widgetToolbarRepository.register( 'image', {
  35. * toolbarItems: editor.config.get( 'image.toolbar' ),
  36. * visibleWhen: viewSelection => isImageWidgetSelected( viewSelection )
  37. * } );
  38. * }
  39. * }
  40. */
  41. export default class WidgetToolbarRepository extends Plugin {
  42. /**
  43. * @inheritDoc
  44. */
  45. static get requires() {
  46. return [ ContextualBalloon ];
  47. }
  48. /**
  49. * @inheritDoc
  50. */
  51. static get pluginName() {
  52. return 'WidgetToolbarRepository';
  53. }
  54. /**
  55. * @inheritDoc
  56. */
  57. init() {
  58. const editor = this.editor;
  59. const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
  60. // Disables the default balloon toolbar for all widgets.
  61. if ( balloonToolbar ) {
  62. this.listenTo( balloonToolbar, 'show', evt => {
  63. if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
  64. evt.stop();
  65. }
  66. }, { priority: 'high' } );
  67. }
  68. /**
  69. * A map of toolbars.
  70. *
  71. * @protected
  72. * @member {Map.<string,Object>} #_toolbars
  73. */
  74. this._toolbars = new Map();
  75. /**
  76. * @private
  77. */
  78. this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
  79. this.listenTo( editor.ui, 'update', () => {
  80. this._updateToolbarsVisibility();
  81. } );
  82. // UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
  83. this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
  84. this._updateToolbarsVisibility();
  85. }, { priority: 'low' } );
  86. }
  87. /**
  88. * Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
  89. * `visibleWhen` function. Toolbar items are gathered from `toolbarItems` array.
  90. * The balloon's CSS class is by default `ck-toolbar-container` and may be override with the `balloonClassName` option.
  91. *
  92. * Note: This method should be called in the {@link module:core/plugin/plugin~Plugin#afterInit `Plugin#afterInit()`} callback (or later)
  93. * to make sure that the given toolbar items were already registered by other plugins.
  94. *
  95. * @param {String} toolbarId An id for the toolbar. Used to
  96. * @param {Object} options
  97. * @param {Array.<String>} options.toolbarItems Array of toolbar items.
  98. * @param {Function} options.visibleWhen Callback which specifies when the toolbar should be visible for the widget.
  99. * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
  100. */
  101. register( toolbarId, { toolbarItems, visibleWhen, balloonClassName = 'ck-toolbar-container' } ) {
  102. const editor = this.editor;
  103. const toolbarView = new ToolbarView();
  104. toolbarView.fillFromConfig( toolbarItems, editor.ui.componentFactory );
  105. if ( this._toolbars.has( toolbarId ) ) {
  106. /**
  107. * Toolbar with the given id was already added.
  108. *
  109. * @error widget-toolbar-duplicated
  110. * @param toolbarId Toolbar id.
  111. */
  112. throw new CKEditorError( 'widget-toolbar-duplicated: Toolbar with the given id was already added.', { toolbarId } );
  113. }
  114. this._toolbars.set( toolbarId, {
  115. view: toolbarView,
  116. visibleWhen,
  117. balloonClassName,
  118. } );
  119. }
  120. /**
  121. * Iterates over stored toolbars and makes them visible or hidden.
  122. *
  123. * @private
  124. */
  125. _updateToolbarsVisibility() {
  126. for ( const toolbar of this._toolbars.values() ) {
  127. if ( !this.editor.ui.focusTracker.isFocused || !toolbar.visibleWhen( this.editor.editing.view.document.selection ) ) {
  128. this._hideToolbar( toolbar );
  129. } else {
  130. this._showToolbar( toolbar );
  131. }
  132. }
  133. }
  134. /**
  135. * Hides the given toolbar.
  136. *
  137. * @private
  138. * @param {Object} toolbar
  139. */
  140. _hideToolbar( toolbar ) {
  141. if ( !this._isToolbarVisible( toolbar ) ) {
  142. return;
  143. }
  144. this._balloon.remove( toolbar.view );
  145. }
  146. /**
  147. * Shows up or repositions the given toolbar.
  148. *
  149. * @private
  150. * @param {Object} toolbar
  151. */
  152. _showToolbar( toolbar ) {
  153. if ( this._isToolbarVisible( toolbar ) ) {
  154. repositionContextualBalloon( this.editor );
  155. } else if ( !this._balloon.hasView( toolbar.view ) ) {
  156. this._balloon.add( {
  157. view: toolbar.view,
  158. position: getBalloonPositionData( this.editor ),
  159. balloonClassName: toolbar.balloonClassName,
  160. } );
  161. }
  162. }
  163. /**
  164. * @private
  165. * @param {Object} toolbar
  166. */
  167. _isToolbarVisible( toolbar ) {
  168. return this._balloon.visibleView == toolbar.view;
  169. }
  170. }
  171. function repositionContextualBalloon( editor ) {
  172. const balloon = editor.plugins.get( 'ContextualBalloon' );
  173. const position = getBalloonPositionData( editor );
  174. balloon.updatePosition( position );
  175. }
  176. function getBalloonPositionData( editor ) {
  177. const editingView = editor.editing.view;
  178. const defaultPositions = BalloonPanelView.defaultPositions;
  179. const widget = getParentWidget( editingView.document.selection );
  180. return {
  181. target: editingView.domConverter.viewToDom( widget ),
  182. positions: [
  183. defaultPositions.northArrowSouth,
  184. defaultPositions.northArrowSouthWest,
  185. defaultPositions.northArrowSouthEast,
  186. defaultPositions.southArrowNorth,
  187. defaultPositions.southArrowNorthWest,
  188. defaultPositions.southArrowNorthEast
  189. ]
  190. };
  191. }
  192. function getParentWidget( selection ) {
  193. const selectedElement = selection.getSelectedElement();
  194. if ( selectedElement && isWidget( selectedElement ) ) {
  195. return selectedElement;
  196. }
  197. const position = selection.getFirstPosition();
  198. let parent = position.parent;
  199. while ( parent ) {
  200. if ( parent.is( 'element' ) && isWidget( parent ) ) {
  201. return parent;
  202. }
  203. parent = parent.parent;
  204. }
  205. }
  206. function isWidgetSelected( selection ) {
  207. const viewElement = selection.getSelectedElement();
  208. return !!( viewElement && isWidget( viewElement ) );
  209. }