widgettoolbarrepository.js 11 KB

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