8
0

contextualtoolbar.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module ui/toolbar/contextual/contextualtoolbar
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import ContextualBalloon from '../../panel/balloon/contextualballoon';
  10. import ToolbarView from '../toolbarview';
  11. import BalloonPanelView from '../../panel/balloon/balloonpanelview.js';
  12. import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
  13. import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
  14. import normalizeToolbarConfig from '../normalizetoolbarconfig';
  15. /**
  16. * The contextual toolbar.
  17. *
  18. * It uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
  19. *
  20. * @extends module:core/plugin~Plugin
  21. */
  22. export default class ContextualToolbar extends Plugin {
  23. /**
  24. * @inheritDoc
  25. */
  26. static get pluginName() {
  27. return 'ContextualToolbar';
  28. }
  29. /**
  30. * @inheritDoc
  31. */
  32. static get requires() {
  33. return [ ContextualBalloon ];
  34. }
  35. /**
  36. * @inheritDoc
  37. */
  38. init() {
  39. const editor = this.editor;
  40. /**
  41. * The toolbar view displayed in the balloon.
  42. *
  43. * @member {module:ui/toolbar/toolbarview~ToolbarView}
  44. */
  45. this.toolbarView = new ToolbarView( editor.locale );
  46. this.toolbarView.extendTemplate( {
  47. attributes: {
  48. class: [
  49. 'ck-editor-toolbar',
  50. 'ck-toolbar_floating'
  51. ]
  52. }
  53. } );
  54. this.toolbarView.render();
  55. /**
  56. * The contextual balloon plugin instance.
  57. *
  58. * @private
  59. * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
  60. */
  61. this._balloon = editor.plugins.get( ContextualBalloon );
  62. /**
  63. * Fires {@link #event:_selectionChangeDebounced} event using `lodash#debounce`.
  64. *
  65. * This function is stored as a plugin property to make possible to cancel
  66. * trailing debounced invocation on destroy.
  67. *
  68. * @private
  69. * @member {Function}
  70. */
  71. this._fireSelectionChangeDebounced = debounce( () => this.fire( '_selectionChangeDebounced' ), 200 );
  72. // Attach lifecycle actions.
  73. this._handleSelectionChange();
  74. this._handleFocusChange();
  75. // The appearance of the ContextualToolbar method is event–driven.
  76. // It is possible to stop the #show event and this prevent the toolbar from showing up.
  77. this.decorate( 'show' );
  78. }
  79. /**
  80. * Creates toolbar components based on given configuration.
  81. * This needs to be done when all plugins are ready.
  82. *
  83. * @inheritDoc
  84. */
  85. afterInit() {
  86. const config = normalizeToolbarConfig( this.editor.config.get( 'contextualToolbar' ) );
  87. const factory = this.editor.ui.componentFactory;
  88. this.toolbarView.fillFromConfig( config.items, factory );
  89. }
  90. /**
  91. * Handles the editor focus change and hides the toolbar if it's needed.
  92. *
  93. * @private
  94. */
  95. _handleFocusChange() {
  96. const editor = this.editor;
  97. // Hide the panel View when editor loses focus but no the other way around.
  98. this.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, isFocused ) => {
  99. if ( this._balloon.visibleView === this.toolbarView && !isFocused ) {
  100. this.hide();
  101. }
  102. } );
  103. }
  104. /**
  105. * Handles {@link module:engine/model/document~Document#selection} change and show or hide toolbar.
  106. *
  107. * Note that in this case it's better to listen to {@link module:engine/model/document~Document model document}
  108. * selection instead of {@link module:engine/view/document~Document view document} selection because the first one
  109. * doesn't fire `change` event after text style change (like bold or italic) and toolbar doesn't blink.
  110. *
  111. * @private
  112. */
  113. _handleSelectionChange() {
  114. const selection = this.editor.model.document.selection;
  115. const editingView = this.editor.editing.view;
  116. this.listenTo( selection, 'change:range', ( evt, data ) => {
  117. // When the selection is not changed by a collaboration and when is not collapsed.
  118. if ( data.directChange || selection.isCollapsed ) {
  119. // Hide the toolbar when the selection starts changing.
  120. this.hide();
  121. }
  122. // Fire internal `_selectionChangeDebounced` when the selection stops changing.
  123. this._fireSelectionChangeDebounced();
  124. } );
  125. // Hide the toolbar when the selection stops changing.
  126. this.listenTo( this, '_selectionChangeDebounced', () => {
  127. // This implementation assumes that only non–collapsed selections gets the contextual toolbar.
  128. if ( editingView.isFocused && !editingView.selection.isCollapsed ) {
  129. this.show();
  130. }
  131. } );
  132. }
  133. /**
  134. * Shows the toolbar and attaches it to the selection.
  135. *
  136. * Fires {@link #event:show} event which can be stopped to prevent the toolbar from showing up.
  137. */
  138. show() {
  139. // Do not add the toolbar to the balloon stack twice.
  140. if ( this._balloon.hasView( this.toolbarView ) ) {
  141. return;
  142. }
  143. // Don not show the toolbar when all components inside are disabled
  144. // see https://github.com/ckeditor/ckeditor5-ui/issues/269.
  145. if ( Array.from( this.toolbarView.items ).every( item => item.isEnabled !== undefined && !item.isEnabled ) ) {
  146. return;
  147. }
  148. // Update the toolbar position upon #render (e.g. external document changes)
  149. // while it's visible.
  150. this.listenTo( this.editor.editing.view, 'render', () => {
  151. this._balloon.updatePosition( this._getBalloonPositionData() );
  152. } );
  153. // Add the toolbar to the common editor contextual balloon.
  154. this._balloon.add( {
  155. view: this.toolbarView,
  156. position: this._getBalloonPositionData(),
  157. balloonClassName: 'ck-toolbar-container ck-editor-toolbar-container'
  158. } );
  159. }
  160. /**
  161. * Hides the toolbar.
  162. */
  163. hide() {
  164. if ( this._balloon.hasView( this.toolbarView ) ) {
  165. this.stopListening( this.editor.editing.view, 'render' );
  166. this._balloon.remove( this.toolbarView );
  167. }
  168. }
  169. /**
  170. * Returns positioning options for the {@link #_balloon}. They control the way balloon is attached
  171. * to the selection.
  172. *
  173. * @private
  174. * @returns {module:utils/dom/position~Options}
  175. */
  176. _getBalloonPositionData() {
  177. const editor = this.editor;
  178. const editingView = editor.editing.view;
  179. // Get direction of the selection.
  180. const isBackward = editingView.selection.isBackward;
  181. return {
  182. // Because the target for BalloonPanelView is a Rect (not DOMRange), it's geometry will stay fixed
  183. // as the window scrolls. To let the BalloonPanelView follow such Rect, is must be continuously
  184. // computed and hence, the target is defined as a function instead of a static value.
  185. // https://github.com/ckeditor/ckeditor5-ui/issues/195
  186. target: () => {
  187. const range = editingView.selection.getFirstRange();
  188. const rangeRects = Rect.getDomRangeRects( editingView.domConverter.viewRangeToDom( range ) );
  189. // Select the proper range rect depending on the direction of the selection.
  190. if ( isBackward ) {
  191. return rangeRects[ 0 ];
  192. } else {
  193. // Ditch the zero-width "orphan" rect in the next line for the forward selection if there's
  194. // another one preceding it. It is not rendered as a selection by the web browser anyway.
  195. // https://github.com/ckeditor/ckeditor5-ui/issues/308
  196. if ( rangeRects.length > 1 && rangeRects[ rangeRects.length - 1 ].width === 0 ) {
  197. rangeRects.pop();
  198. }
  199. return rangeRects[ rangeRects.length - 1 ];
  200. }
  201. },
  202. positions: getBalloonPositions( isBackward )
  203. };
  204. }
  205. /**
  206. * @inheritDoc
  207. */
  208. destroy() {
  209. this._fireSelectionChangeDebounced.cancel();
  210. this.stopListening();
  211. super.destroy();
  212. }
  213. /**
  214. * This event is fired just before the toolbar shows up. Stopping this event will prevent this.
  215. *
  216. * @event show
  217. */
  218. /**
  219. * This is internal plugin event which is fired 200 ms after model selection last change.
  220. * This is to makes easy test debounced action without need to use `setTimeout`.
  221. *
  222. * @protected
  223. * @event _selectionChangeDebounced
  224. */
  225. }
  226. // Returns toolbar positions for the given direction of the selection.
  227. //
  228. // @private
  229. // @param {Boolean} isBackward
  230. // @returns {Array.<module:utils/dom/position~Position>}
  231. function getBalloonPositions( isBackward ) {
  232. const defaultPositions = BalloonPanelView.defaultPositions;
  233. return isBackward ? [
  234. defaultPositions.northWestArrowSouth,
  235. defaultPositions.northWestArrowSouthWest,
  236. defaultPositions.northWestArrowSouthEast,
  237. defaultPositions.southWestArrowNorth,
  238. defaultPositions.southWestArrowNorthWest,
  239. defaultPositions.southWestArrowNorthEast
  240. ] : [
  241. defaultPositions.southEastArrowNorth,
  242. defaultPositions.southEastArrowNorthEast,
  243. defaultPositions.southEastArrowNorthWest,
  244. defaultPositions.northEastArrowSouth,
  245. defaultPositions.northEastArrowSouthEast,
  246. defaultPositions.northEastArrowSouthWest
  247. ];
  248. }
  249. /**
  250. * Contextual toolbar configuration. Used by the {@link module:ui/toolbar/contextual/contextualtoolbar~ContextualToolbar}
  251. * feature.
  252. *
  253. * const config = {
  254. * contextualToolbar: [ 'bold', 'italic', 'undo', 'redo' ]
  255. * };
  256. *
  257. * You can also use `'|'` to create a separator between groups of items:
  258. *
  259. * const config = {
  260. * contextualToolbar: [ 'bold', 'italic', | 'undo', 'redo' ]
  261. * };
  262. *
  263. * Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
  264. *
  265. * @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#contextualToolbar
  266. */