8
0

integration.js 13 KB

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