ballooneditorui.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals document, Event */
  6. import BalloonEditorUI from '../src/ballooneditorui';
  7. import EditorUI from '@ckeditor/ckeditor5-core/src/editor/editorui';
  8. import BalloonEditorUIView from '../src/ballooneditoruiview';
  9. import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
  10. import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloontoolbar';
  11. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  12. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  13. import { isElement } from 'lodash-es';
  14. import { assertBinding } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  15. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
  16. describe( 'BalloonEditorUI', () => {
  17. let editor, view, ui, viewElement;
  18. testUtils.createSinonSandbox();
  19. beforeEach( () => {
  20. return VirtualBalloonTestEditor
  21. .create( 'foo', {
  22. plugins: [ BalloonToolbar ]
  23. } )
  24. .then( newEditor => {
  25. editor = newEditor;
  26. ui = editor.ui;
  27. view = ui.view;
  28. viewElement = view.editable.element;
  29. } );
  30. } );
  31. afterEach( () => {
  32. editor.destroy();
  33. } );
  34. describe( 'constructor()', () => {
  35. it( 'extends EditorUI', () => {
  36. expect( ui ).to.instanceof( EditorUI );
  37. } );
  38. } );
  39. describe( 'init()', () => {
  40. it( 'initializes the #view', () => {
  41. expect( view.isRendered ).to.be.true;
  42. } );
  43. it( 'initializes keyboard navigation between view#toolbar and view#editable', () => {
  44. const toolbar = editor.plugins.get( 'BalloonToolbar' );
  45. const toolbarFocusSpy = testUtils.sinon.stub( toolbar.toolbarView, 'focus' ).returns( {} );
  46. const toolbarShowSpy = testUtils.sinon.stub( toolbar, 'show' ).returns( {} );
  47. const toolbarHideSpy = testUtils.sinon.stub( toolbar, 'hide' ).returns( {} );
  48. const editingFocusSpy = testUtils.sinon.stub( editor.editing.view, 'focus' ).returns( {} );
  49. ui.focusTracker.isFocused = true;
  50. // #show and #hide are mocked so mocking the focus as well.
  51. toolbar.toolbarView.focusTracker.isFocused = false;
  52. editor.keystrokes.press( {
  53. keyCode: keyCodes.f10,
  54. altKey: true,
  55. preventDefault: sinon.spy(),
  56. stopPropagation: sinon.spy()
  57. } );
  58. sinon.assert.callOrder( toolbarShowSpy, toolbarFocusSpy );
  59. sinon.assert.notCalled( toolbarHideSpy );
  60. sinon.assert.notCalled( editingFocusSpy );
  61. // #show and #hide are mocked so mocking the focus as well.
  62. toolbar.toolbarView.focusTracker.isFocused = true;
  63. toolbar.toolbarView.keystrokes.press( {
  64. keyCode: keyCodes.esc,
  65. preventDefault: sinon.spy(),
  66. stopPropagation: sinon.spy()
  67. } );
  68. sinon.assert.callOrder( editingFocusSpy, toolbarHideSpy );
  69. } );
  70. describe( 'editable', () => {
  71. let editable;
  72. beforeEach( () => {
  73. editable = editor.editing.view.document.getRoot();
  74. } );
  75. it( 'registers view.editable#element in editor focus tracker', () => {
  76. ui.focusTracker.isFocused = false;
  77. view.editable.element.dispatchEvent( new Event( 'focus' ) );
  78. expect( ui.focusTracker.isFocused ).to.true;
  79. } );
  80. it( 'sets view.editable#name', () => {
  81. expect( view.editable.name ).to.equal( editable.rootName );
  82. } );
  83. it( 'binds view.editable#isFocused', () => {
  84. assertBinding(
  85. view.editable,
  86. { isFocused: false },
  87. [
  88. [ ui.focusTracker, { isFocused: true } ]
  89. ],
  90. { isFocused: true }
  91. );
  92. } );
  93. } );
  94. describe( 'placeholder', () => {
  95. it( 'sets placeholder from editor.config.placeholder', () => {
  96. return VirtualBalloonTestEditor
  97. .create( 'foo', {
  98. extraPlugins: [ BalloonToolbar, Paragraph ],
  99. placeholder: 'placeholder-text'
  100. } )
  101. .then( newEditor => {
  102. const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
  103. expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
  104. return newEditor.destroy();
  105. } );
  106. } );
  107. it( 'sets placeholder from the "placeholder" attribute of a passed <textarea>', () => {
  108. const element = document.createElement( 'textarea' );
  109. element.setAttribute( 'placeholder', 'placeholder-text' );
  110. return VirtualBalloonTestEditor
  111. .create( element, {
  112. plugins: [ BalloonToolbar ],
  113. extraPlugins: [ Paragraph ]
  114. } )
  115. .then( newEditor => {
  116. const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
  117. expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'placeholder-text' );
  118. return newEditor.destroy();
  119. } );
  120. } );
  121. it( 'uses editor.config.placeholder rather than the "placeholder" attribute of a passed <textarea>', () => {
  122. const element = document.createElement( 'textarea' );
  123. element.setAttribute( 'placeholder', 'placeholder-text' );
  124. return VirtualBalloonTestEditor
  125. .create( element, {
  126. plugins: [ BalloonToolbar ],
  127. extraPlugins: [ Paragraph ],
  128. placeholder: 'config takes precedence'
  129. } )
  130. .then( newEditor => {
  131. const firstChild = newEditor.editing.view.document.getRoot().getChild( 0 );
  132. expect( firstChild.getAttribute( 'data-placeholder' ) ).to.equal( 'config takes precedence' );
  133. return newEditor.destroy();
  134. } );
  135. } );
  136. } );
  137. } );
  138. describe( 'destroy()', () => {
  139. it( 'detaches the DOM root then destroys the UI view', () => {
  140. return VirtualBalloonTestEditor
  141. .create( '', {
  142. plugins: [ BalloonToolbar ]
  143. } )
  144. .then( newEditor => {
  145. const destroySpy = sinon.spy( newEditor.ui.view, 'destroy' );
  146. const detachSpy = sinon.spy( newEditor.editing.view, 'detachDomRoot' );
  147. return newEditor.destroy()
  148. .then( () => {
  149. sinon.assert.callOrder( detachSpy, destroySpy );
  150. } );
  151. } );
  152. } );
  153. it( 'restores the editor element back to its original state', () => {
  154. const domElement = document.createElement( 'div' );
  155. domElement.setAttribute( 'foo', 'bar' );
  156. domElement.setAttribute( 'data-baz', 'qux' );
  157. domElement.classList.add( 'foo-class' );
  158. return VirtualBalloonTestEditor
  159. .create( domElement, {
  160. plugins: [ BalloonToolbar ]
  161. } )
  162. .then( newEditor => {
  163. return newEditor.destroy()
  164. .then( () => {
  165. const attributes = {};
  166. for ( const attribute of Array.from( domElement.attributes ) ) {
  167. attributes[ attribute.name ] = attribute.value;
  168. }
  169. expect( attributes ).to.deep.equal( {
  170. foo: 'bar',
  171. 'data-baz': 'qux',
  172. class: 'foo-class'
  173. } );
  174. } );
  175. } );
  176. } );
  177. } );
  178. describe( 'element()', () => {
  179. it( 'returns correct element instance', () => {
  180. expect( ui.element ).to.equal( viewElement );
  181. } );
  182. } );
  183. describe( 'getEditableElement()', () => {
  184. it( 'returns editable element (default)', () => {
  185. expect( ui.getEditableElement() ).to.equal( view.editable.element );
  186. } );
  187. it( 'returns editable element (root name passed)', () => {
  188. expect( ui.getEditableElement( 'main' ) ).to.equal( view.editable.element );
  189. } );
  190. it( 'returns undefined if editable with the given name is absent', () => {
  191. expect( ui.getEditableElement( 'absent' ) ).to.be.undefined;
  192. } );
  193. } );
  194. } );
  195. class VirtualBalloonTestEditor extends VirtualTestEditor {
  196. constructor( sourceElementOrData, config ) {
  197. super( config );
  198. if ( isElement( sourceElementOrData ) ) {
  199. this.sourceElement = sourceElementOrData;
  200. }
  201. const view = new BalloonEditorUIView( this.locale, this.editing.view );
  202. this.ui = new BalloonEditorUI( this, view );
  203. }
  204. destroy() {
  205. this.ui.destroy();
  206. return super.destroy();
  207. }
  208. static create( sourceElementOrData, config ) {
  209. return new Promise( resolve => {
  210. const editor = new this( sourceElementOrData, config );
  211. resolve(
  212. editor.initPlugins()
  213. .then( () => {
  214. editor.ui.init();
  215. const initialData = isElement( sourceElementOrData ) ?
  216. sourceElementOrData.innerHTML :
  217. sourceElementOrData;
  218. editor.data.init( initialData );
  219. editor.fire( 'ready' );
  220. } )
  221. .then( () => editor )
  222. );
  223. } );
  224. }
  225. }