contextualballoon.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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/classiceditor';
  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. }
  126. _showPanel() {
  127. if ( this._balloon.hasView( this.view ) ) {
  128. return;
  129. }
  130. const viewDocument = this.editor.editing.view;
  131. this._balloon.add( {
  132. view: this.view,
  133. position: {
  134. target: viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() )
  135. }
  136. } );
  137. }
  138. _hidePanel() {
  139. if ( !this._balloon.hasView( this.view ) ) {
  140. return;
  141. }
  142. this._balloon.remove( this.view );
  143. }
  144. }
  145. class PluginA extends PluginGeneric {
  146. init() {
  147. this.label = 'PluginA';
  148. this.view = new ViewA( this.editor.locale );
  149. this.buttons = [ 'PluginB' ];
  150. super.init();
  151. }
  152. }
  153. class PluginB extends PluginGeneric {
  154. init() {
  155. this.label = 'PluginB';
  156. this.view = new ViewA( this.editor.locale );
  157. this.buttons = [ 'PluginC' ];
  158. super.init();
  159. }
  160. }
  161. class PluginC extends PluginGeneric {
  162. init() {
  163. this.label = 'PluginC';
  164. this.view = new ViewA( this.editor.locale );
  165. this.buttons = [ 'PluginD' ];
  166. super.init();
  167. }
  168. }
  169. class PluginD extends PluginGeneric {
  170. init() {
  171. this.label = 'PluginD';
  172. this.view = new ViewA( this.editor.locale );
  173. super.init();
  174. }
  175. }
  176. // Create contextual toolbar.
  177. function createContextualToolbar( editor ) {
  178. const balloon = editor.plugins.get( ContextualBalloon );
  179. const toolbar = new ToolbarView();
  180. const editingView = editor.editing.view;
  181. const defaultPositions = BalloonPanelView.defaultPositions;
  182. // Add plugins to the toolbar.
  183. toolbar.fillFromConfig( [ 'PluginA', 'PluginB' ], editor.ui.componentFactory );
  184. // Close toolbar when selection is changing.
  185. editor.listenTo( editingView, 'selectionChange', () => close() );
  186. // Handle when selection stop changing.
  187. editor.listenTo( editingView, 'selectionChangeDone', () => {
  188. // This implementation assumes that only non–collapsed selections gets the contextual toolbar.
  189. if ( !editingView.selection.isCollapsed ) {
  190. const isBackward = editingView.selection.isBackward;
  191. const rangeRects = editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ).getClientRects();
  192. balloon.add( {
  193. view: toolbar,
  194. position: {
  195. target: isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 ),
  196. positions: [ defaultPositions[ isBackward ? 'northArrowSouth' : 'southArrowNorth' ] ]
  197. }
  198. } );
  199. }
  200. } );
  201. // Remove toolbar from balloon.
  202. function close() {
  203. if ( balloon.hasView( toolbar ) ) {
  204. balloon.remove( toolbar );
  205. }
  206. }
  207. }
  208. // Finally the editor.
  209. ClassicEditor.create( document.querySelector( '#editor' ), {
  210. plugins: [ EssentialsPresets, Paragraph, PluginA, PluginB, PluginC, PluginD ],
  211. toolbar: [ 'PluginA', 'PluginB' ]
  212. } )
  213. .then( editor => {
  214. window.editor = editor;
  215. createContextualToolbar( editor );
  216. } )
  217. .catch( err => {
  218. console.error( err.stack );
  219. } );