contextualballoon.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals window, document, console:false */
  6. // This test implements contextual toolbar and few plugins which can be opened inside of the toolbar.
  7. // Code of this manual test should be successfully reduced when more of
  8. // CKE5 plugins will be integrated with ContextualBalloon and when
  9. // ContextualToolbar plugin will land as CKE5 plugin.
  10. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic';
  11. import EssentialsPresets from '@ckeditor/ckeditor5-presets/src/essentials';
  12. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  13. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  14. import BalloonPanelView from '../../../src/panel/balloon/balloonpanelview';
  15. import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
  16. import ToolbarView from '../../../src/toolbar/toolbarview';
  17. import ButtonView from '../../../src/button/buttonview';
  18. import Template from '../../../src/template';
  19. import View from '../../../src/view';
  20. import clickOutsideHandler from '../../../src/bindings/clickoutsidehandler';
  21. import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
  22. // Plugin view which displays toolbar with component to open next
  23. // plugin inside and cancel button to close currently visible plugin.
  24. class ViewA extends View {
  25. constructor( locale ) {
  26. super( locale );
  27. this.keystrokes = new KeystrokeHandler();
  28. this.toolbar = this.createCollection();
  29. this.keystrokes.set( 'Esc', ( data, cancel ) => {
  30. this.fire( 'cancel' );
  31. cancel();
  32. } );
  33. this.cancel = new ButtonView( locale );
  34. this.cancel.label = 'Cancel';
  35. this.cancel.withText = true;
  36. this.cancel.on( 'execute', () => this.fire( 'cancel' ) );
  37. this.template = new Template( {
  38. tag: 'div',
  39. attributes: {
  40. class: [ 'plugin', this.bindTemplate.to( 'label' ) ],
  41. tabindex: '-1'
  42. },
  43. children: [
  44. {
  45. tag: 'div',
  46. children: [
  47. {
  48. tag: 'h2',
  49. children: [
  50. { text: this.bindTemplate.to( 'label' ) },
  51. ]
  52. },
  53. {
  54. tag: 'div',
  55. attributes: {
  56. class: [ 'toolbar' ]
  57. },
  58. children: [
  59. {
  60. tag: 'h3',
  61. children: [
  62. { text: 'Open:' },
  63. ]
  64. },
  65. {
  66. tag: 'div',
  67. children: this.toolbar
  68. },
  69. ]
  70. },
  71. this.cancel
  72. ]
  73. }
  74. ]
  75. } );
  76. }
  77. init() {
  78. this.keystrokes.listenTo( this.element );
  79. return super.init();
  80. }
  81. }
  82. // Generic plugin class.
  83. class PluginGeneric extends Plugin {
  84. static get requires() {
  85. return [ ContextualBalloon ];
  86. }
  87. init() {
  88. this._balloon = this.editor.plugins.get( ContextualBalloon );
  89. this.editor.editing.view.on( 'selectionChange', () => this._hidePanel() );
  90. this.view.bind( 'label' ).to( this );
  91. this.view.on( 'cancel', () => {
  92. if ( this._isVisible ) {
  93. this._hidePanel();
  94. }
  95. } );
  96. this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
  97. if ( this._isVisible ) {
  98. this._hidePanel();
  99. }
  100. cancel();
  101. } );
  102. this.editor.ui.componentFactory.add( this.label, locale => {
  103. const button = new ButtonView( locale );
  104. button.label = this.label;
  105. button.withText = true;
  106. this.listenTo( button, 'execute', () => this._showPanel() );
  107. return button;
  108. } );
  109. clickOutsideHandler( {
  110. emitter: this.view,
  111. activator: () => this._isVisible,
  112. contextElement: this.view.element,
  113. callback: () => this._hidePanel()
  114. } );
  115. }
  116. get _isVisible() {
  117. return this._balloon.visibleView === this.view;
  118. }
  119. afterInit() {
  120. if ( this.buttons ) {
  121. const toolbar = new ToolbarView();
  122. this.view.toolbar.add( toolbar );
  123. return toolbar.fillFromConfig( this.buttons, this.editor.ui.componentFactory );
  124. }
  125. return Promise.resolve();
  126. }
  127. _showPanel() {
  128. if ( this._balloon.hasView( this.view ) ) {
  129. return;
  130. }
  131. const viewDocument = this.editor.editing.view;
  132. this._balloon.add( {
  133. view: this.view,
  134. position: {
  135. target: viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() )
  136. }
  137. } );
  138. }
  139. _hidePanel() {
  140. if ( !this._balloon.hasView( this.view ) ) {
  141. return;
  142. }
  143. this._balloon.remove( this.view );
  144. }
  145. }
  146. class PluginA extends PluginGeneric {
  147. init() {
  148. this.label = 'PluginA';
  149. this.view = new ViewA( this.editor.locale );
  150. this.buttons = [ 'PluginB' ];
  151. super.init();
  152. }
  153. }
  154. class PluginB extends PluginGeneric {
  155. init() {
  156. this.label = 'PluginB';
  157. this.view = new ViewA( this.editor.locale );
  158. this.buttons = [ 'PluginC' ];
  159. super.init();
  160. }
  161. }
  162. class PluginC extends PluginGeneric {
  163. init() {
  164. this.label = 'PluginC';
  165. this.view = new ViewA( this.editor.locale );
  166. this.buttons = [ 'PluginD' ];
  167. super.init();
  168. }
  169. }
  170. class PluginD extends PluginGeneric {
  171. init() {
  172. this.label = 'PluginD';
  173. this.view = new ViewA( this.editor.locale );
  174. super.init();
  175. }
  176. }
  177. // Create contextual toolbar.
  178. function createContextualToolbar( editor ) {
  179. const balloon = editor.plugins.get( ContextualBalloon );
  180. const toolbar = new ToolbarView();
  181. const editingView = editor.editing.view;
  182. const defaultPositions = BalloonPanelView.defaultPositions;
  183. // Add plugins to the toolbar.
  184. toolbar.fillFromConfig( [ 'PluginA', 'PluginB' ], editor.ui.componentFactory );
  185. // Close toolbar when selection is changing.
  186. editor.listenTo( editingView, 'selectionChange', () => close() );
  187. // Handle when selection stop changing.
  188. editor.listenTo( editingView, 'selectionChangeDone', () => {
  189. // This implementation assumes that only non–collapsed selections gets the contextual toolbar.
  190. if ( !editingView.selection.isCollapsed ) {
  191. const isBackward = editingView.selection.isBackward;
  192. const rangeRects = editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ).getClientRects();
  193. balloon.add( {
  194. view: toolbar,
  195. position: {
  196. target: isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 ),
  197. positions: [ defaultPositions[ isBackward ? 'northArrowSouth' : 'southArrowNorth' ] ]
  198. }
  199. } );
  200. }
  201. } );
  202. // Remove toolbar from balloon.
  203. function close() {
  204. if ( balloon.hasView( toolbar ) ) {
  205. balloon.remove( toolbar );
  206. }
  207. }
  208. }
  209. // Finally the editor.
  210. ClassicEditor.create( document.querySelector( '#editor' ), {
  211. plugins: [ EssentialsPresets, Paragraph, PluginA, PluginB, PluginC, PluginD ],
  212. toolbar: [ 'PluginA', 'PluginB' ]
  213. } )
  214. .then( editor => {
  215. window.editor = editor;
  216. createContextualToolbar( editor );
  217. } )
  218. .catch( err => {
  219. console.error( err.stack );
  220. } );