8
0

contextualballoon.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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/panel/balloon/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.editor.editing.view.on( 'selectionChange', () => this._hidePanel() );
  89. this.view.bind( 'label' ).to( this );
  90. this.view.on( 'cancel', () => {
  91. if ( this._isVisible ) {
  92. this._hidePanel();
  93. }
  94. } );
  95. this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
  96. if ( this._isVisible ) {
  97. this._hidePanel();
  98. }
  99. cancel();
  100. } );
  101. this.editor.ui.componentFactory.add( this.label, ( locale ) => {
  102. const button = new ButtonView( locale );
  103. button.label = this.label;
  104. button.withText = true;
  105. this.listenTo( button, 'execute', () => this._showPanel() );
  106. return button;
  107. } );
  108. clickOutsideHandler( {
  109. emitter: this.view,
  110. activator: () => this._isVisible,
  111. contextElement: this.view.element,
  112. callback: () => this._hidePanel()
  113. } );
  114. }
  115. get _isVisible() {
  116. return this._balloon.visibleView === this.view;
  117. }
  118. afterInit() {
  119. if ( this.buttons ) {
  120. const toolbar = new ToolbarView();
  121. this.view.toolbar.add( toolbar );
  122. return toolbar.fillFromConfig( this.buttons, this.editor.ui.componentFactory );
  123. }
  124. return Promise.resolve();
  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 arrowVOffset = 20;
  182. const positions = {
  183. forwardSelection: ( targetRect, balloonRect ) => ( {
  184. top: targetRect.bottom + arrowVOffset,
  185. left: targetRect.right - balloonRect.width / 2,
  186. name: 'arrow_s'
  187. } ),
  188. backwardSelection: ( targetRect, balloonRect ) => ( {
  189. top: targetRect.top - balloonRect.height - arrowVOffset,
  190. left: targetRect.left - balloonRect.width / 2,
  191. name: 'arrow_n'
  192. } )
  193. };
  194. // Add plugins to the toolbar.
  195. toolbar.fillFromConfig( [ 'PluginA', 'PluginB' ], editor.ui.componentFactory );
  196. // Close toolbar when selection is changing.
  197. editor.listenTo( editingView, 'selectionChange', () => close() );
  198. // Handle when selection stop changing.
  199. editor.listenTo( editingView, 'selectionChangeDone', () => {
  200. // This implementation assumes that only non–collapsed selections gets the contextual toolbar.
  201. if ( !editingView.selection.isCollapsed ) {
  202. const isBackward = editingView.selection.isBackward;
  203. const rangeRects = editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ).getClientRects();
  204. balloon.add( {
  205. view: toolbar,
  206. position: {
  207. target: isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 ),
  208. positions: [ positions[ isBackward ? 'backwardSelection' : 'forwardSelection' ] ]
  209. }
  210. } );
  211. }
  212. } );
  213. // Remove toolbar from balloon.
  214. function close() {
  215. if ( balloon.hasView( toolbar ) ) {
  216. balloon.remove( toolbar );
  217. }
  218. }
  219. }
  220. // Finally the editor.
  221. ClassicEditor.create( document.querySelector( '#editor' ), {
  222. plugins: [ EssentialsPresets, Paragraph, PluginA, PluginB, PluginC, PluginD ],
  223. toolbar: [ 'PluginA', 'PluginB' ]
  224. } )
  225. .then( editor => {
  226. window.editor = editor;
  227. createContextualToolbar( editor );
  228. } )
  229. .catch( err => {
  230. console.error( err.stack );
  231. } );