integration.js 14 KB

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