8
0

contextualballoon.js 6.8 KB

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