8
0

contextualballoon.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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 Template from '../../../src/template';
  15. import View from '../../../src/view';
  16. import ToolbarView from '../../../src/toolbar/toolbarview';
  17. import ButtonView from '../../../src/button/buttonview';
  18. import clickOutsideHandler from '../../../src/bindings/clickoutsidehandler';
  19. import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
  20. // Plugin view which displays toolbar with next component to open and
  21. // cancel button to close currently visible plugin.
  22. class ViewA extends View {
  23. constructor( locale ) {
  24. super( locale );
  25. this.keystrokes = new KeystrokeHandler();
  26. this.toolbar = this.createCollection();
  27. this.keystrokes.set( 'Esc', ( data, cancel ) => {
  28. this.fire( 'cancel' );
  29. cancel();
  30. } );
  31. this.cancel = new ButtonView( locale );
  32. this.cancel.label = 'Cancel';
  33. this.cancel.withText = true;
  34. this.cancel.on( 'execute', () => this.fire( 'cancel' ) );
  35. this.template = new Template( {
  36. tag: 'div',
  37. attributes: {
  38. class: [ 'plugin', this.bindTemplate.to( 'label' ) ],
  39. tabindex: '-1'
  40. },
  41. children: [
  42. {
  43. tag: 'div',
  44. children: [
  45. {
  46. tag: 'h2',
  47. children: [
  48. { text: this.bindTemplate.to( 'label' ) },
  49. ]
  50. },
  51. {
  52. tag: 'div',
  53. attributes: {
  54. class: [ 'toolbar' ]
  55. },
  56. children: [
  57. {
  58. tag: 'h3',
  59. children: [
  60. { text: 'Open:' },
  61. ]
  62. },
  63. {
  64. tag: 'div',
  65. children: this.toolbar
  66. },
  67. ]
  68. },
  69. this.cancel
  70. ]
  71. }
  72. ]
  73. } );
  74. }
  75. init() {
  76. this.keystrokes.listenTo( this.element );
  77. return super.init();
  78. }
  79. }
  80. // Generic plugin class.
  81. class PluginGeneric extends Plugin {
  82. init() {
  83. this._isOpen = false;
  84. this.editor.editing.view.on( 'selectionChange', () => this._hidePanel() );
  85. this.view.bind( 'label' ).to( this );
  86. this.view.on( 'cancel', () => {
  87. if ( this.editor.ui.balloon.visible.view === this.view ) {
  88. this._hidePanel();
  89. }
  90. } );
  91. this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
  92. if ( this.editor.ui.balloon.visible.view === this.view ) {
  93. this._hidePanel();
  94. }
  95. cancel();
  96. } );
  97. this.editor.ui.componentFactory.add( this.label, ( locale ) => {
  98. const button = new ButtonView( locale );
  99. button.label = this.label;
  100. button.withText = true;
  101. this.listenTo( button, 'execute', () => this._showPanel() );
  102. return button;
  103. } );
  104. clickOutsideHandler( {
  105. emitter: this.view,
  106. activator: () => this._isOpen && this.editor.ui.balloon.visible.view === this.view,
  107. contextElement: this.view.element,
  108. callback: () => this._hidePanel()
  109. } );
  110. }
  111. afterInit() {
  112. if ( this.buttons ) {
  113. const toolbar = new ToolbarView();
  114. this.view.toolbar.add( toolbar );
  115. return toolbar.fillFromConfig( this.buttons, this.editor.ui.componentFactory );
  116. }
  117. return Promise.resolve();
  118. }
  119. _showPanel() {
  120. if ( this._isOpen ) {
  121. return;
  122. }
  123. const viewDocument = this.editor.editing.view;
  124. this.editor.ui.balloon.add( {
  125. view: this.view,
  126. position: {
  127. target: viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() )
  128. }
  129. } );
  130. this._isOpen = true;
  131. }
  132. _hidePanel() {
  133. if ( !this._isOpen ) {
  134. return;
  135. }
  136. this.editor.ui.balloon.remove( this.view );
  137. this._isOpen = false;
  138. }
  139. }
  140. class PluginA extends PluginGeneric {
  141. init() {
  142. this.label = 'PluginA';
  143. this.view = new ViewA( this.editor.locale );
  144. this.buttons = [ 'PluginB' ];
  145. super.init();
  146. }
  147. }
  148. class PluginB extends PluginGeneric {
  149. init() {
  150. this.label = 'PluginB';
  151. this.view = new ViewA( this.editor.locale );
  152. this.buttons = [ 'PluginC' ];
  153. super.init();
  154. }
  155. }
  156. class PluginC extends PluginGeneric {
  157. init() {
  158. this.label = 'PluginC';
  159. this.view = new ViewA( this.editor.locale );
  160. this.buttons = [ 'PluginD' ];
  161. super.init();
  162. }
  163. }
  164. class PluginD extends PluginGeneric {
  165. init() {
  166. this.label = 'PluginD';
  167. this.view = new ViewA( this.editor.locale );
  168. super.init();
  169. }
  170. }
  171. // Create contextual toolbar.
  172. function createContextualToolbar( editor ) {
  173. const toolbar = new ToolbarView();
  174. const editingView = editor.editing.view;
  175. const arrowVOffset = 20;
  176. const positions = {
  177. forwardSelection: ( targetRect, balloonRect ) => ( {
  178. top: targetRect.bottom + arrowVOffset,
  179. left: targetRect.right - balloonRect.width / 2,
  180. name: 's'
  181. } ),
  182. backwardSelection: ( targetRect, balloonRect ) => ( {
  183. top: targetRect.top - balloonRect.height - arrowVOffset,
  184. left: targetRect.left - balloonRect.width / 2,
  185. name: 'n'
  186. } )
  187. };
  188. let isOpen = false;
  189. // Add plugins to the toolbar.
  190. toolbar.fillFromConfig( [ 'PluginA', 'PluginB' ], editor.ui.componentFactory );
  191. // Close toolbar when selection is changing.
  192. editor.listenTo( editingView, 'selectionChange', () => close() );
  193. // Handle when selection stop changing.
  194. editor.listenTo( editingView, 'selectionChangeDone', () => {
  195. // This implementation assumes that only non–collapsed selections gets the contextual toolbar.
  196. if ( !editingView.selection.isCollapsed ) {
  197. const isBackward = editingView.selection.isBackward;
  198. const rangeRects = editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ).getClientRects();
  199. isOpen = true;
  200. editor.ui.balloon.add( {
  201. view: toolbar,
  202. position: {
  203. target: isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 ),
  204. positions: [ positions[ isBackward ? 'backwardSelection' : 'forwardSelection' ] ]
  205. }
  206. } );
  207. }
  208. } );
  209. // Remove toolbar from balloon and mark as not opened.
  210. function close() {
  211. if ( isOpen ) {
  212. editor.ui.balloon.remove( toolbar );
  213. }
  214. isOpen = false;
  215. }
  216. }
  217. // Finally the editor.
  218. ClassicEditor.create( document.querySelector( '#editor' ), {
  219. plugins: [ EssentialsPresets, Paragraph, PluginA, PluginB, PluginC, PluginD ],
  220. toolbar: [ 'PluginA', 'PluginB' ]
  221. } )
  222. .then( editor => {
  223. window.editor = editor;
  224. createContextualToolbar( editor );
  225. } )
  226. .catch( err => {
  227. console.error( err.stack );
  228. } );