integration.js 13 KB

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