widgettoolbarrepository.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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 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 registering widget toolbars. This plugin handles the whole
  16. * toolbar rendering process and exposes a concise API.
  17. *
  18. * To add a toolbar for your widget use the {@link ~WidgetToolbarRepository#register `WidgetToolbarRepository#register()`} method.
  19. *
  20. * The following example comes from the {@link module:image/imagetoolbar~ImageToolbar} plugin:
  21. *
  22. * class ImageToolbar extends Plugin {
  23. * static get requires() {
  24. * return [ WidgetToolbarRepository ];
  25. * }
  26. *
  27. * afterInit() {
  28. * const editor = this.editor;
  29. * const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
  30. *
  31. * widgetToolbarRepository.register( 'image', {
  32. * items: editor.config.get( 'image.toolbar' ),
  33. * getRelatedElement: getSelectedImageWidget
  34. * } );
  35. * }
  36. * }
  37. */
  38. export default class WidgetToolbarRepository extends Plugin {
  39. /**
  40. * @inheritDoc
  41. */
  42. static get requires() {
  43. return [ ContextualBalloon ];
  44. }
  45. /**
  46. * @inheritDoc
  47. */
  48. static get pluginName() {
  49. return 'WidgetToolbarRepository';
  50. }
  51. /**
  52. * @inheritDoc
  53. */
  54. init() {
  55. const editor = this.editor;
  56. // Disables the default balloon toolbar for all widgets.
  57. if ( editor.plugins.has( 'BalloonToolbar' ) ) {
  58. const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
  59. this.listenTo( balloonToolbar, 'show', evt => {
  60. if ( isWidgetSelected( editor.editing.view.document.selection ) ) {
  61. evt.stop();
  62. }
  63. }, { priority: 'high' } );
  64. }
  65. /**
  66. * A map of toolbar definitions.
  67. *
  68. * @protected
  69. * @member {Map.<String,module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition>} #_toolbarDefinitions
  70. */
  71. this._toolbarDefinitions = new Map();
  72. /**
  73. * @private
  74. */
  75. this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
  76. this.listenTo( editor.ui, 'update', () => {
  77. this._updateToolbarsVisibility();
  78. } );
  79. // UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
  80. this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
  81. this._updateToolbarsVisibility();
  82. }, { priority: 'low' } );
  83. }
  84. destroy() {
  85. super.destroy();
  86. for ( const toolbarConfig of this._toolbarDefinitions.values() ) {
  87. toolbarConfig.view.destroy();
  88. }
  89. }
  90. /**
  91. * Registers toolbar in the WidgetToolbarRepository. It renders it in the `ContextualBalloon` based on the value of the invoked
  92. * `getRelatedElement` function. Toolbar items are gathered from `items` array.
  93. * The balloon's CSS class is by default `ck-toolbar-container` and may be override with the `balloonClassName` option.
  94. *
  95. * Note: This method should be called in the {@link module:core/plugin~PluginInterface#afterInit `Plugin#afterInit()`}
  96. * callback (or later) to make sure that the given toolbar items were already registered by other plugins.
  97. *
  98. * @param {String} toolbarId An id for the toolbar. Used to
  99. * @param {Object} options
  100. * @param {Array.<String>} options.items Array of toolbar items.
  101. * @param {Function} options.getRelatedElement Callback which returns an element the toolbar should be attached to.
  102. * @param {String} [options.balloonClassName='ck-toolbar-container'] CSS class for the widget balloon.
  103. */
  104. register( toolbarId, { items, getRelatedElement, balloonClassName = 'ck-toolbar-container' } ) {
  105. const editor = this.editor;
  106. const toolbarView = new ToolbarView();
  107. if ( this._toolbarDefinitions.has( toolbarId ) ) {
  108. /**
  109. * Toolbar with the given id was already added.
  110. *
  111. * @error widget-toolbar-duplicated
  112. * @param toolbarId Toolbar id.
  113. */
  114. throw new CKEditorError( 'widget-toolbar-duplicated: Toolbar with the given id was already added.', { toolbarId } );
  115. }
  116. toolbarView.fillFromConfig( items, editor.ui.componentFactory );
  117. this._toolbarDefinitions.set( toolbarId, {
  118. view: toolbarView,
  119. getRelatedElement,
  120. balloonClassName,
  121. } );
  122. }
  123. /**
  124. * Iterates over stored toolbars and makes them visible or hidden.
  125. *
  126. * @private
  127. */
  128. _updateToolbarsVisibility() {
  129. let maxRelatedElementDepth = 0;
  130. let deepestRelatedElement = null;
  131. let deepestToolbarDefinition = null;
  132. for ( const definition of this._toolbarDefinitions.values() ) {
  133. const relatedElement = definition.getRelatedElement( this.editor.editing.view.document.selection );
  134. if ( !this.editor.ui.focusTracker.isFocused ) {
  135. if ( this._isToolbarVisible( definition ) ) {
  136. this._hideToolbar( definition );
  137. }
  138. } else if ( !relatedElement ) {
  139. if ( this._isToolbarInBalloon( definition ) ) {
  140. this._hideToolbar( definition );
  141. }
  142. } else {
  143. const relatedElementDepth = relatedElement.getAncestors().length;
  144. // Many toolbars can express willingness to be displayed but they do not know about
  145. // each other. Figure out which toolbar is deepest in the view tree to decide which
  146. // should be displayed. For instance, if a selected image is inside a table cell, display
  147. // the ImageToolbar rather than the TableToolbar (#60).
  148. if ( relatedElementDepth > maxRelatedElementDepth ) {
  149. maxRelatedElementDepth = relatedElementDepth;
  150. deepestRelatedElement = relatedElement;
  151. deepestToolbarDefinition = definition;
  152. }
  153. }
  154. }
  155. if ( deepestToolbarDefinition ) {
  156. this._showToolbar( deepestToolbarDefinition, deepestRelatedElement );
  157. }
  158. }
  159. /**
  160. * Hides the given toolbar.
  161. *
  162. * @private
  163. * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
  164. */
  165. _hideToolbar( toolbarDefinition ) {
  166. this._balloon.remove( toolbarDefinition.view );
  167. }
  168. /**
  169. * Shows up the toolbar if the toolbar is not visible.
  170. * Otherwise, repositions the toolbar's balloon when toolbar's view is the most top view in balloon stack.
  171. *
  172. * It might happen here that the toolbar's view is under another view. Then do nothing as the other toolbar view
  173. * should be still visible after the {@link module:core/editor/editorui~EditorUI#event:update}.
  174. *
  175. * @private
  176. * @param {module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition} toolbarDefinition
  177. * @param {module:engine/view/element~Element} relatedElement
  178. */
  179. _showToolbar( toolbarDefinition, relatedElement ) {
  180. if ( this._isToolbarVisible( toolbarDefinition ) ) {
  181. repositionContextualBalloon( this.editor, relatedElement );
  182. } else if ( !this._isToolbarInBalloon( toolbarDefinition.view ) ) {
  183. this._balloon.add( {
  184. view: toolbarDefinition.view,
  185. position: getBalloonPositionData( this.editor, relatedElement ),
  186. balloonClassName: toolbarDefinition.balloonClassName,
  187. } );
  188. }
  189. }
  190. /**
  191. * @private
  192. * @param {Object} toolbar
  193. * @returns {Boolean}
  194. */
  195. _isToolbarVisible( toolbar ) {
  196. return this._balloon.visibleView === toolbar.view;
  197. }
  198. /**
  199. * @private
  200. * @param {Object} toolbar
  201. * @returns {Boolean}
  202. */
  203. _isToolbarInBalloon( toolbar ) {
  204. return this._balloon.hasView( toolbar.view );
  205. }
  206. }
  207. function repositionContextualBalloon( editor, relatedElement ) {
  208. const balloon = editor.plugins.get( 'ContextualBalloon' );
  209. const position = getBalloonPositionData( editor, relatedElement );
  210. balloon.updatePosition( position );
  211. }
  212. function getBalloonPositionData( editor, relatedElement ) {
  213. const editingView = editor.editing.view;
  214. const defaultPositions = BalloonPanelView.defaultPositions;
  215. return {
  216. target: editingView.domConverter.viewToDom( relatedElement ),
  217. positions: [
  218. defaultPositions.northArrowSouth,
  219. defaultPositions.northArrowSouthWest,
  220. defaultPositions.northArrowSouthEast,
  221. defaultPositions.southArrowNorth,
  222. defaultPositions.southArrowNorthWest,
  223. defaultPositions.southArrowNorthEast
  224. ]
  225. };
  226. }
  227. function isWidgetSelected( selection ) {
  228. const viewElement = selection.getSelectedElement();
  229. return !!( viewElement && isWidget( viewElement ) );
  230. }
  231. /**
  232. * The toolbar definition object used by the toolbar repository to manage toolbars.
  233. * It contains information necessary to display the toolbar in the
  234. * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} and
  235. * update it during its life (display) cycle.
  236. *
  237. * @typedef {Object} module:widget/widgettoolbarrepository~WidgetRepositoryToolbarDefinition
  238. *
  239. * @property {module:ui/view~View} view The UI view of the toolbar.
  240. * @property {Function} getRelatedElement A function that returns an engine {@link module:engine/view/view~View}
  241. * element the toolbar is to be attached to. For instance, an image widget or a table widget (or `null` when
  242. * there is no such element). The function accepts an instance of {@link module:engine/view/selection~Selection}.
  243. * @property {String} balloonClassName CSS class for the widget balloon when a toolbar is displayed.
  244. */