integration.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* global document */
  6. import BlockQuote from '../src/blockquote';
  7. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  8. import Image from '@ckeditor/ckeditor5-image/src/image';
  9. import ImageCaption from '@ckeditor/ckeditor5-image/src/imagecaption';
  10. import List from '@ckeditor/ckeditor5-list/src/list';
  11. import Enter from '@ckeditor/ckeditor5-enter/src/enter';
  12. import Delete from '@ckeditor/ckeditor5-typing/src/delete';
  13. import Heading from '@ckeditor/ckeditor5-heading/src/heading';
  14. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  15. import {
  16. parse as parseModel,
  17. getData as getModelData,
  18. setData as setModelData
  19. } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  20. describe( 'BlockQuote integration', () => {
  21. let editor, model, element;
  22. beforeEach( () => {
  23. element = document.createElement( 'div' );
  24. document.body.appendChild( element );
  25. return ClassicTestEditor
  26. .create( element, {
  27. plugins: [ BlockQuote, Paragraph, Image, ImageCaption, List, Enter, Delete, Heading ]
  28. } )
  29. .then( newEditor => {
  30. editor = newEditor;
  31. model = editor.model;
  32. } );
  33. } );
  34. afterEach( () => {
  35. element.remove();
  36. return editor.destroy();
  37. } );
  38. describe( 'enter key support', () => {
  39. function fakeEventData() {
  40. return {
  41. preventDefault: sinon.spy()
  42. };
  43. }
  44. it( 'does nothing if selection is in an empty block but not in a block quote', () => {
  45. const data = fakeEventData();
  46. const execSpy = sinon.spy( editor, 'execute' );
  47. setModelData( model, '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>x</paragraph>' );
  48. editor.editing.view.fire( 'enter', data );
  49. // Only enter command should be executed.
  50. expect( data.preventDefault.called ).to.be.true;
  51. expect( execSpy.calledOnce ).to.be.true;
  52. expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' );
  53. } );
  54. it( 'does nothing if selection is in a non-empty block (at the end) in a block quote', () => {
  55. const data = fakeEventData();
  56. const execSpy = sinon.spy( editor, 'execute' );
  57. setModelData( model, '<blockQuote><paragraph>xx[]</paragraph></blockQuote>' );
  58. editor.editing.view.fire( 'enter', data );
  59. // Only enter command should be executed.
  60. expect( data.preventDefault.called ).to.be.true;
  61. expect( execSpy.calledOnce ).to.be.true;
  62. expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' );
  63. } );
  64. it( 'does nothing if selection is in a non-empty block (at the beginning) in a block quote', () => {
  65. const data = fakeEventData();
  66. const execSpy = sinon.spy( editor, 'execute' );
  67. setModelData( model, '<blockQuote><paragraph>[]xx</paragraph></blockQuote>' );
  68. editor.editing.view.fire( 'enter', data );
  69. // Only enter command should be executed.
  70. expect( data.preventDefault.called ).to.be.true;
  71. expect( execSpy.calledOnce ).to.be.true;
  72. expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' );
  73. } );
  74. it( 'does nothing if selection is not collapsed', () => {
  75. const data = fakeEventData();
  76. const execSpy = sinon.spy( editor, 'execute' );
  77. setModelData( model, '<blockQuote><paragraph>[</paragraph><paragraph>]</paragraph></blockQuote>' );
  78. editor.editing.view.fire( 'enter', data );
  79. // Only enter command should be executed.
  80. expect( data.preventDefault.called ).to.be.true;
  81. expect( execSpy.calledOnce ).to.be.true;
  82. expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'enter' );
  83. } );
  84. it( 'does not interfere with a similar handler in the list feature', () => {
  85. const data = fakeEventData();
  86. setModelData( model,
  87. '<paragraph>x</paragraph>' +
  88. '<blockQuote>' +
  89. '<listItem indent="0" type="bulleted">a</listItem>' +
  90. '<listItem indent="0" type="bulleted">[]</listItem>' +
  91. '</blockQuote>' +
  92. '<paragraph>x</paragraph>'
  93. );
  94. editor.editing.view.fire( 'enter', data );
  95. expect( data.preventDefault.called ).to.be.true;
  96. expect( getModelData( model ) ).to.equal(
  97. '<paragraph>x</paragraph>' +
  98. '<blockQuote>' +
  99. '<listItem indent="0" type="bulleted">a</listItem>' +
  100. '<paragraph>[]</paragraph>' +
  101. '</blockQuote>' +
  102. '<paragraph>x</paragraph>'
  103. );
  104. } );
  105. it( 'escapes block quote if selection is in an empty block in an empty block quote', () => {
  106. const data = fakeEventData();
  107. const execSpy = sinon.spy( editor, 'execute' );
  108. setModelData( model, '<paragraph>x</paragraph><blockQuote><paragraph>[]</paragraph></blockQuote><paragraph>x</paragraph>' );
  109. editor.editing.view.fire( 'enter', data );
  110. expect( data.preventDefault.called ).to.be.true;
  111. expect( execSpy.calledOnce ).to.be.true;
  112. expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' );
  113. expect( getModelData( model ) ).to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>x</paragraph>' );
  114. } );
  115. it( 'escapes block quote if selection is in an empty block in the middle of a block quote', () => {
  116. const data = fakeEventData();
  117. const execSpy = sinon.spy( editor, 'execute' );
  118. setModelData( model,
  119. '<paragraph>x</paragraph>' +
  120. '<blockQuote><paragraph>a</paragraph><paragraph>[]</paragraph><paragraph>b</paragraph></blockQuote>' +
  121. '<paragraph>x</paragraph>'
  122. );
  123. editor.editing.view.fire( 'enter', data );
  124. expect( data.preventDefault.called ).to.be.true;
  125. expect( execSpy.calledOnce ).to.be.true;
  126. expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' );
  127. expect( getModelData( model ) ).to.equal(
  128. '<paragraph>x</paragraph>' +
  129. '<blockQuote><paragraph>a</paragraph></blockQuote>' +
  130. '<paragraph>[]</paragraph>' +
  131. '<blockQuote><paragraph>b</paragraph></blockQuote>' +
  132. '<paragraph>x</paragraph>'
  133. );
  134. } );
  135. it( 'escapes block quote if selection is in an empty block at the end of a block quote', () => {
  136. const data = fakeEventData();
  137. const execSpy = sinon.spy( editor, 'execute' );
  138. setModelData( model,
  139. '<paragraph>x</paragraph>' +
  140. '<blockQuote><paragraph>a</paragraph><paragraph>[]</paragraph></blockQuote>' +
  141. '<paragraph>x</paragraph>'
  142. );
  143. editor.editing.view.fire( 'enter', data );
  144. expect( data.preventDefault.called ).to.be.true;
  145. expect( execSpy.calledOnce ).to.be.true;
  146. expect( execSpy.args[ 0 ][ 0 ] ).to.equal( 'blockQuote' );
  147. expect( getModelData( model ) ).to.equal(
  148. '<paragraph>x</paragraph>' +
  149. '<blockQuote><paragraph>a</paragraph></blockQuote>' +
  150. '<paragraph>[]</paragraph>' +
  151. '<paragraph>x</paragraph>'
  152. );
  153. } );
  154. it( 'scrolls the view document to the selection after the command is executed', () => {
  155. const data = fakeEventData();
  156. const execSpy = sinon.spy( editor, 'execute' );
  157. const scrollSpy = sinon.stub( editor.editing.view, 'scrollToTheSelection' );
  158. setModelData( model,
  159. '<paragraph>x</paragraph>' +
  160. '<blockQuote><paragraph>a</paragraph><paragraph>[]</paragraph></blockQuote>' +
  161. '<paragraph>x</paragraph>'
  162. );
  163. editor.editing.view.fire( 'enter', data );
  164. sinon.assert.calledOnce( scrollSpy );
  165. sinon.assert.callOrder( execSpy, scrollSpy );
  166. } );
  167. } );
  168. describe( 'backspace key support', () => {
  169. function fakeEventData() {
  170. return {
  171. preventDefault: sinon.spy(),
  172. direction: 'backward',
  173. unit: 'character'
  174. };
  175. }
  176. it( 'merges paragraph into paragraph in the quote', () => {
  177. const data = fakeEventData();
  178. setModelData( model,
  179. '<blockQuote><paragraph>a</paragraph><paragraph>b</paragraph></blockQuote>' +
  180. '<paragraph>[]c</paragraph>' +
  181. '<paragraph>d</paragraph>'
  182. );
  183. editor.editing.view.fire( 'delete', data );
  184. expect( getModelData( model ) ).to.equal(
  185. '<blockQuote><paragraph>a</paragraph><paragraph>b[]c</paragraph></blockQuote>' +
  186. '<paragraph>d</paragraph>'
  187. );
  188. } );
  189. it( 'merges paragraph from a quote into a paragraph before quote', () => {
  190. const data = fakeEventData();
  191. setModelData( model,
  192. '<paragraph>x</paragraph>' +
  193. '<blockQuote><paragraph>[]a</paragraph><paragraph>b</paragraph></blockQuote>' +
  194. '<paragraph>y</paragraph>'
  195. );
  196. editor.editing.view.fire( 'delete', data );
  197. expect( getModelData( model ) ).to.equal(
  198. '<paragraph>x[]a</paragraph>' +
  199. '<blockQuote><paragraph>b</paragraph></blockQuote>' +
  200. '<paragraph>y</paragraph>'
  201. );
  202. } );
  203. it( 'merges two quotes', () => {
  204. const data = fakeEventData();
  205. setModelData( model,
  206. '<paragraph>x</paragraph>' +
  207. '<blockQuote><paragraph>a</paragraph><paragraph>b</paragraph></blockQuote>' +
  208. '<blockQuote><paragraph>[]c</paragraph><paragraph>d</paragraph></blockQuote>' +
  209. '<paragraph>y</paragraph>'
  210. );
  211. editor.editing.view.fire( 'delete', data );
  212. expect( getModelData( model ) ).to.equal(
  213. '<paragraph>x</paragraph>' +
  214. '<blockQuote><paragraph>a</paragraph><paragraph>b[]c</paragraph><paragraph>d</paragraph></blockQuote>' +
  215. '<paragraph>y</paragraph>'
  216. );
  217. } );
  218. it( 'removes empty quote when merging into another quote', () => {
  219. const data = fakeEventData();
  220. setModelData( model,
  221. '<paragraph>x</paragraph>' +
  222. '<blockQuote><paragraph>a</paragraph></blockQuote>' +
  223. '<blockQuote><paragraph>[]</paragraph></blockQuote>' +
  224. '<paragraph>y</paragraph>'
  225. );
  226. editor.editing.view.fire( 'delete', data );
  227. expect( getModelData( model ) ).to.equal(
  228. '<paragraph>x</paragraph>' +
  229. '<blockQuote><paragraph>a[]</paragraph></blockQuote>' +
  230. '<paragraph>y</paragraph>'
  231. );
  232. } );
  233. it( 'removes empty quote when merging into a paragraph', () => {
  234. const data = fakeEventData();
  235. setModelData( model,
  236. '<paragraph>x</paragraph>' +
  237. '<blockQuote><paragraph>[]</paragraph></blockQuote>' +
  238. '<paragraph>y</paragraph>'
  239. );
  240. editor.editing.view.fire( 'delete', data );
  241. expect( getModelData( model ) ).to.equal(
  242. '<paragraph>x[]</paragraph>' +
  243. '<paragraph>y</paragraph>'
  244. );
  245. } );
  246. } );
  247. // Historically, due to problems with schema, images were not quotable.
  248. // These tests were left here to confirm that after schema was fixed, images are properly quotable.
  249. describe( 'compatibility with images', () => {
  250. it( 'quotes a simple image', () => {
  251. const element = document.createElement( 'div' );
  252. document.body.appendChild( element );
  253. // We can't load ImageCaption in this test because it adds <caption> to all images automatically.
  254. return ClassicTestEditor
  255. .create( element, {
  256. plugins: [ BlockQuote, Paragraph, Image ]
  257. } )
  258. .then( editor => {
  259. setModelData( editor.model,
  260. '<paragraph>fo[o</paragraph>' +
  261. '<image src="foo.png"></image>' +
  262. '<paragraph>b]ar</paragraph>'
  263. );
  264. editor.execute( 'blockQuote' );
  265. expect( getModelData( editor.model ) ).to.equal(
  266. '<blockQuote>' +
  267. '<paragraph>fo[o</paragraph>' +
  268. '<image src="foo.png"></image>' +
  269. '<paragraph>b]ar</paragraph>' +
  270. '</blockQuote>'
  271. );
  272. element.remove();
  273. return editor.destroy();
  274. } );
  275. } );
  276. it( 'quotes an image with caption', () => {
  277. setModelData( model,
  278. '<paragraph>fo[o</paragraph>' +
  279. '<image src="foo.png">' +
  280. '<caption>xxx</caption>' +
  281. '</image>' +
  282. '<paragraph>b]ar</paragraph>'
  283. );
  284. editor.execute( 'blockQuote' );
  285. expect( getModelData( model ) ).to.equal(
  286. '<blockQuote>' +
  287. '<paragraph>fo[o</paragraph>' +
  288. '<image src="foo.png">' +
  289. '<caption>xxx</caption>' +
  290. '</image>' +
  291. '<paragraph>b]ar</paragraph>' +
  292. '</blockQuote>'
  293. );
  294. } );
  295. it( 'adds an image to an existing quote', () => {
  296. setModelData( model,
  297. '<paragraph>fo[o</paragraph>' +
  298. '<image src="foo.png">' +
  299. '<caption>xxx</caption>' +
  300. '</image>' +
  301. '<blockQuote><paragraph>b]ar</paragraph></blockQuote>'
  302. );
  303. editor.execute( 'blockQuote' );
  304. expect( getModelData( model ) ).to.equal(
  305. '<blockQuote>' +
  306. '<paragraph>fo[o</paragraph>' +
  307. '<image src="foo.png">' +
  308. '<caption>xxx</caption>' +
  309. '</image>' +
  310. '<paragraph>b]ar</paragraph>' +
  311. '</blockQuote>'
  312. );
  313. } );
  314. } );
  315. // When blockQuote with a paragraph was pasted into a list item, the item contained the paragraph. It was invalid.
  316. // There is a test which checks whether blockQuote will split the list items instead of merging with.
  317. describe( 'compatibility with lists', () => {
  318. it( 'does not merge the paragraph with list item', () => {
  319. setModelData( model, '<listItem indent="0" type="bulleted">fo[]o</listItem>' );
  320. const df = parseModel(
  321. '<blockQuote><paragraph>xxx</paragraph></blockQuote><heading1>yyy</heading1>',
  322. model.schema
  323. );
  324. model.insertContent( df, model.document.selection );
  325. expect( getModelData( model ) ).to.equal(
  326. '<listItem indent="0" type="bulleted">fo</listItem>' +
  327. '<blockQuote>' +
  328. '<paragraph>xxx</paragraph>' +
  329. '</blockQuote>' +
  330. '<heading1>yyy[]o</heading1>'
  331. );
  332. } );
  333. } );
  334. } );