imageinsertui.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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 */
  6. import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
  7. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  8. import Image from '../../src/image';
  9. import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
  10. import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview';
  11. import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
  12. import ImageInsert from '../../src/imageinsert';
  13. import ImageInsertUI from '../../src/imageinsert/imageinsertui';
  14. import ImageInsertPanelView from '../../src/imageinsert/ui/imageinsertpanelview';
  15. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  16. import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
  17. import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
  18. import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
  19. import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
  20. import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
  21. import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
  22. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  23. import { UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
  24. describe( 'ImageInsertUI', () => {
  25. let editor, editorElement, fileRepository, dropdown;
  26. class UploadAdapterPluginMock extends Plugin {
  27. init() {
  28. fileRepository = this.editor.plugins.get( FileRepository );
  29. fileRepository.createUploadAdapter = loader => {
  30. return new UploadAdapterMock( loader );
  31. };
  32. }
  33. }
  34. describe( 'dropdown', () => {
  35. beforeEach( () => {
  36. editorElement = document.createElement( 'div' );
  37. document.body.appendChild( editorElement );
  38. return ClassicEditor
  39. .create( editorElement, {
  40. plugins: [ Paragraph, Image, ImageInsert, ImageInsertUI, FileRepository, UploadAdapterPluginMock, Clipboard ],
  41. toolbar: [ 'imageInsert' ],
  42. image: {
  43. insert: {
  44. integrations: [
  45. 'insertImageViaUrl'
  46. ]
  47. }
  48. }
  49. } )
  50. .then( newEditor => {
  51. editor = newEditor;
  52. dropdown = editor.ui.view.toolbar.children.first.children.first;
  53. // Hide all notifications (prevent alert() calls).
  54. const notification = editor.plugins.get( Notification );
  55. notification.on( 'show', evt => evt.stop() );
  56. } );
  57. } );
  58. afterEach( () => {
  59. editorElement.remove();
  60. return editor.destroy();
  61. } );
  62. it( 'should register the "imageInsert" dropdown', () => {
  63. const button = editor.ui.componentFactory.create( 'imageInsert' );
  64. expect( button ).to.be.instanceOf( DropdownView );
  65. } );
  66. it( 'should not insert panel view children until dropdown is not open for the first time', () => {
  67. expect( dropdown.panelView.children.length ).to.equal( 0 );
  68. dropdown.buttonView.fire( 'open' );
  69. expect( dropdown.panelView.children.length ).to.equal( 1 );
  70. expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertPanelView );
  71. } );
  72. describe( 'dropdown action button', () => {
  73. it( 'should be an instance of FileDialogButtonView', () => {
  74. const dropdown = editor.ui.componentFactory.create( 'imageInsert' );
  75. expect( dropdown.buttonView.actionView ).to.be.instanceOf( FileDialogButtonView );
  76. } );
  77. } );
  78. describe( 'dropdown panel buttons', () => {
  79. it( 'should have "Update" label on submit button when URL input is already filled', () => {
  80. const viewDocument = editor.editing.view.document;
  81. editor.setData( '<figure><img src="/assets/sample.png" /></figure>' );
  82. editor.editing.view.change( writer => {
  83. writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
  84. } );
  85. const img = viewDocument.selection.getSelectedElement();
  86. const data = fakeEventData();
  87. const eventInfo = new EventInfo( img, 'click' );
  88. const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
  89. viewDocument.fire( 'click', domEventDataMock );
  90. dropdown.buttonView.fire( 'open' );
  91. const inputValue = dropdown.panelView.children.first.imageURLInputValue;
  92. expect( inputValue ).to.equal( '/assets/sample.png' );
  93. expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Update' );
  94. } );
  95. it( 'should have "Insert" label on submit button on uploading a new image', () => {
  96. const viewDocument = editor.editing.view.document;
  97. editor.setData( '<p>test</p>' );
  98. editor.editing.view.change( writer => {
  99. writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' );
  100. } );
  101. const el = viewDocument.selection.getSelectedElement();
  102. const data = fakeEventData();
  103. const eventInfo = new EventInfo( el, 'click' );
  104. const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
  105. viewDocument.fire( 'click', domEventDataMock );
  106. dropdown.buttonView.fire( 'open' );
  107. const inputValue = dropdown.panelView.children.first.imageURLInputValue;
  108. expect( dropdown.isOpen ).to.be.true;
  109. expect( inputValue ).to.equal( '' );
  110. expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Insert' );
  111. } );
  112. } );
  113. describe( 'dropdown panel integrations', () => {
  114. describe( 'insert image via URL form', () => {
  115. it( 'should have "Insert image via URL" label on inserting new image', () => {
  116. const viewDocument = editor.editing.view.document;
  117. editor.setData( '<p>test</p>' );
  118. editor.editing.view.change( writer => {
  119. writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' );
  120. } );
  121. const el = viewDocument.selection.getSelectedElement();
  122. const data = fakeEventData();
  123. const eventInfo = new EventInfo( el, 'click' );
  124. const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
  125. viewDocument.fire( 'click', domEventDataMock );
  126. dropdown.buttonView.fire( 'open' );
  127. const inputValue = dropdown.panelView.children.first.imageURLInputValue;
  128. const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' );
  129. expect( dropdown.isOpen ).to.be.true;
  130. expect( inputValue ).to.equal( '' );
  131. expect( insertImageViaUrlForm.label ).to.equal( 'Insert image via URL' );
  132. } );
  133. it( 'should have "Update image URL" label on updating the image source URL', () => {
  134. const viewDocument = editor.editing.view.document;
  135. editor.setData( '<figure><img src="/assets/sample.png" /></figure>' );
  136. editor.editing.view.change( writer => {
  137. writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
  138. } );
  139. const el = viewDocument.selection.getSelectedElement();
  140. const data = fakeEventData();
  141. const eventInfo = new EventInfo( el, 'click' );
  142. const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
  143. viewDocument.fire( 'click', domEventDataMock );
  144. dropdown.buttonView.fire( 'open' );
  145. const inputValue = dropdown.panelView.children.first.imageURLInputValue;
  146. const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' );
  147. expect( dropdown.isOpen ).to.be.true;
  148. expect( inputValue ).to.equal( '/assets/sample.png' );
  149. expect( insertImageViaUrlForm.label ).to.equal( 'Update image URL' );
  150. } );
  151. } );
  152. } );
  153. it( 'should remove all attributes from model except "src" when updating the image source URL', () => {
  154. const viewDocument = editor.editing.view.document;
  155. const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
  156. const submitSpy = sinon.spy();
  157. dropdown.buttonView.fire( 'open' );
  158. const insertButtonView = dropdown.panelView.children.first.insertButtonView;
  159. editor.setData( '<figure><img src="image-url-800w.jpg"' +
  160. 'srcset="image-url-480w.jpg 480w,image-url-800w.jpg 800w"' +
  161. 'sizes="(max-width: 600px) 480px,800px"' +
  162. 'alt="test-image"></figure>' );
  163. editor.editing.view.change( writer => {
  164. writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
  165. } );
  166. const selectedElement = editor.model.document.selection.getSelectedElement();
  167. expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'image-url-800w.jpg' );
  168. expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.true;
  169. dropdown.panelView.children.first.imageURLInputValue = '/assets/sample3.png';
  170. dropdown.on( 'submit', submitSpy );
  171. insertButtonView.fire( 'execute' );
  172. sinon.assert.notCalled( commandSpy );
  173. sinon.assert.calledOnce( submitSpy );
  174. expect( dropdown.isOpen ).to.be.false;
  175. expect( selectedElement.getAttribute( 'src' ) ).to.equal( '/assets/sample3.png' );
  176. expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false;
  177. expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false;
  178. } );
  179. describe( 'events', () => {
  180. it( 'should emit "submit" event when clicking on submit button', () => {
  181. const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
  182. const submitSpy = sinon.spy();
  183. dropdown.buttonView.fire( 'open' );
  184. dropdown.on( 'submit', submitSpy );
  185. const insertButtonView = dropdown.panelView.children.first.insertButtonView;
  186. insertButtonView.fire( 'execute' );
  187. expect( dropdown.isOpen ).to.be.false;
  188. sinon.assert.calledOnce( commandSpy );
  189. sinon.assert.calledOnce( submitSpy );
  190. } );
  191. it( 'should emit "cancel" event when clicking on cancel button', () => {
  192. const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
  193. const cancelSpy = sinon.spy();
  194. dropdown.buttonView.fire( 'open' );
  195. dropdown.on( 'cancel', cancelSpy );
  196. const cancelButtonView = dropdown.panelView.children.first.cancelButtonView;
  197. cancelButtonView.fire( 'execute' );
  198. expect( dropdown.isOpen ).to.be.false;
  199. sinon.assert.notCalled( commandSpy );
  200. sinon.assert.calledOnce( cancelSpy );
  201. } );
  202. it( 'should focus on "insert image via URL" input after opening', () => {
  203. let spy;
  204. // The ImageInsertPanelView is added on first open.
  205. // See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652
  206. dropdown.on( 'change:isOpen', () => {
  207. const imageInsertPanelView = dropdown.panelView.children.first;
  208. spy = sinon.spy( imageInsertPanelView, 'focus' );
  209. }, { priority: 'highest' } );
  210. dropdown.buttonView.fire( 'open' );
  211. sinon.assert.calledOnce( spy );
  212. } );
  213. } );
  214. it( 'should inject integrations to the dropdown panel view from the config', async () => {
  215. const editor = await ClassicEditor
  216. .create( editorElement, {
  217. plugins: [
  218. CKFinder,
  219. Paragraph,
  220. Image,
  221. ImageInsert,
  222. ImageInsertUI,
  223. FileRepository,
  224. UploadAdapterPluginMock,
  225. Clipboard
  226. ],
  227. image: {
  228. insert: {
  229. integrations: [
  230. 'insertImageViaUrl',
  231. 'openCKFinder'
  232. ]
  233. }
  234. }
  235. } );
  236. const dropdown = editor.ui.componentFactory.create( 'imageInsert' );
  237. dropdown.buttonView.fire( 'open' );
  238. expect( dropdown.panelView.children.first._integrations.length ).to.equal( 2 );
  239. expect( dropdown.panelView.children.first._integrations.first ).to.be.instanceOf( LabeledFieldView );
  240. expect( dropdown.panelView.children.first._integrations.last ).to.be.instanceOf( ButtonView );
  241. editor.destroy();
  242. } );
  243. } );
  244. } );
  245. function fakeEventData() {
  246. return {
  247. preventDefault: sinon.spy()
  248. };
  249. }