title.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* global document */
  6. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  7. import Title from '../src/title';
  8. import Enter from '@ckeditor/ckeditor5-enter/src/enter';
  9. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  10. import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
  11. import Heading from '@ckeditor/ckeditor5-heading/src/heading';
  12. import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
  13. import Image from '@ckeditor/ckeditor5-image/src/image';
  14. import ImageUpload from '@ckeditor/ckeditor5-image/src/imageupload';
  15. import Undo from '@ckeditor/ckeditor5-undo/src/undo';
  16. import DataTransfer from '@ckeditor/ckeditor5-clipboard/src/datatransfer';
  17. import { UploadAdapterMock, createNativeFileMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
  18. import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  19. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  20. describe( 'Title', () => {
  21. let element, editor, model;
  22. beforeEach( () => {
  23. element = document.createElement( 'div' );
  24. document.body.appendChild( element );
  25. return ClassicTestEditor.create( element, {
  26. plugins: [ Title, Heading, BlockQuote, Clipboard, Image, ImageUpload, Enter, Undo ]
  27. } ).then( _editor => {
  28. editor = _editor;
  29. model = editor.model;
  30. } );
  31. } );
  32. afterEach( () => {
  33. return editor.destroy().then( () => element.remove() );
  34. } );
  35. it( 'should requires certain plugins', () => {
  36. expect( Title.requires ).to.have.members( [ Paragraph, Enter ] );
  37. } );
  38. it( 'should have plugin name property', () => {
  39. expect( Title.pluginName ).to.equal( 'Title' );
  40. } );
  41. it( 'should set proper schema rules', () => {
  42. expect( model.schema.isRegistered( 'title', '$text' ) ).to.equal( true );
  43. expect( model.schema.checkChild( [ 'title' ], '$text' ) ).to.equal( true );
  44. expect( model.schema.checkChild( [ '$root' ], 'title' ) ).to.equal( true );
  45. expect( model.schema.checkChild( [ '$root', 'blockQuote' ], 'title' ) ).to.equal( false );
  46. expect( model.schema.checkChild( [ '$root', 'paragraph' ], 'title' ) ).to.equal( false );
  47. } );
  48. it( 'should convert title to h1', () => {
  49. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  50. expect( editor.getData() ).to.equal( '<h1>Foo</h1><p>Bar</p>' );
  51. } );
  52. describe( 'model post-fixing', () => {
  53. it( 'should set title and content elements', () => {
  54. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  55. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph>' );
  56. } );
  57. it( 'should create a content element when only title has been set', () => {
  58. setData( model, '<title>Foo</title>' );
  59. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph></paragraph>' );
  60. } );
  61. it( 'should create a title and content elements when are missing', () => {
  62. setData( model, '' );
  63. expect( getData( model ) ).to.equal( '<title>[]</title><paragraph></paragraph>' );
  64. } );
  65. it( 'should change heading1 element to title when is set as a first root child', () => {
  66. setData( model, '<heading1>Foo</heading1><heading1>Bar</heading1>' );
  67. expect( getData( model ) ).to.equal( '<title>[]Foo</title><heading1>Bar</heading1>' );
  68. } );
  69. it( 'should change heading2 element to title when is set as a first root child', () => {
  70. setData( model, '<heading2>Foo</heading2><heading2>Bar</heading2>' );
  71. expect( getData( model ) ).to.equal( '<title>[]Foo</title><heading2>Bar</heading2>' );
  72. } );
  73. it( 'should change heading3 element to title when is set as a first root child', () => {
  74. setData( model, '<heading3>Foo</heading3><heading3>Bar</heading3>' );
  75. expect( getData( model ) ).to.equal( '<title>[]Foo</title><heading3>Bar</heading3>' );
  76. } );
  77. it( 'should change paragraph element to title when is set as a first root child', () => {
  78. setData( model, '<paragraph>Foo</paragraph><paragraph>Bar</paragraph>' );
  79. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph>' );
  80. } );
  81. it( 'should change title element to a paragraph when is not a first root child #1', () => {
  82. setData( model, '<title>Foo</title><title>Bar</title>' );
  83. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph>' );
  84. } );
  85. it( 'should change title element to a paragraph when is not a first root child #2', () => {
  86. setData( model, '<title>Foo</title><paragraph>Bar</paragraph><title>Biz</title>' );
  87. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Bar</paragraph><paragraph>Biz</paragraph>' );
  88. } );
  89. it( 'should move element after a title element when is not allowed to be a title', () => {
  90. setData( model, '<blockQuote><paragraph>Foo</paragraph></blockQuote><title>Bar</title>' );
  91. expect( getData( model ) ).to.equal( '<title>[]Bar</title><blockQuote><paragraph>Foo</paragraph></blockQuote>' );
  92. } );
  93. it( 'should create a missing title element before an element that cannot to be a title element', () => {
  94. setData( model, '<blockQuote><paragraph>Foo</paragraph></blockQuote>' );
  95. expect( getData( model ) ).to.equal( '<title>[]</title><blockQuote><paragraph>Foo</paragraph></blockQuote>' );
  96. } );
  97. it( 'should clear element from disallowed attributes when changing to title element', () => {
  98. model.schema.extend( '$text', { allowAttributes: 'bold' } );
  99. model.schema.addAttributeCheck( ( context, attributeName ) => {
  100. if ( context.endsWith( 'title $text' ) && attributeName === 'bold' ) {
  101. return false;
  102. }
  103. } );
  104. setData( model,
  105. '<paragraph>F<$text bold="true">o</$text>o</paragraph><paragraph>B<$text bold="true">a</$text>r</paragraph>'
  106. );
  107. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>B<$text bold="true">a</$text>r</paragraph>' );
  108. } );
  109. } );
  110. describe( 'setTitle()', () => {
  111. it( 'should set new content of a title element', () => {
  112. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  113. editor.plugins.get( 'Title' ).setTitle( 'Biz' );
  114. expect( getData( model ) ).to.equal( '<title>[]Biz</title><paragraph>Bar</paragraph>' );
  115. } );
  116. it( 'should clear content of a title element', () => {
  117. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  118. editor.plugins.get( 'Title' ).setTitle( '' );
  119. expect( getData( model ) ).to.equal( '<title>[]</title><paragraph>Bar</paragraph>' );
  120. } );
  121. } );
  122. describe( 'getTitle()', () => {
  123. it( 'should return content of a title element', () => {
  124. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  125. expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal( 'Foo' );
  126. } );
  127. it( 'should return content of an empty title element', () => {
  128. setData( model, '<title></title><paragraph>Bar</paragraph>' );
  129. expect( editor.plugins.get( 'Title' ).getTitle() ).to.equal( '' );
  130. } );
  131. } );
  132. describe( 'setBody()', () => {
  133. it( 'should set new content to body', () => {
  134. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  135. editor.plugins.get( 'Title' ).setBody( 'Biz' );
  136. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph>Biz</paragraph>' );
  137. } );
  138. it( 'should set empty content to body', () => {
  139. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  140. editor.plugins.get( 'Title' ).setBody( '' );
  141. expect( getData( model ) ).to.equal( '<title>[]Foo</title><paragraph></paragraph>' );
  142. } );
  143. it( 'should set html content to body', () => {
  144. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  145. editor.plugins.get( 'Title' ).setBody( '<blockQuote>Bar</blockQuote><p>Biz</p>' );
  146. expect( getData( model ) ).to.equal(
  147. '<title>[]Foo</title><blockQuote><paragraph>Bar</paragraph></blockQuote><paragraph>Biz</paragraph>'
  148. );
  149. } );
  150. } );
  151. describe( 'getBody()', () => {
  152. it( 'should return all data except the title element', () => {
  153. setData( model, '<title>Foo</title><paragraph>Bar</paragraph><paragraph>Biz</paragraph>' );
  154. expect( editor.plugins.get( 'Title' ).getBody() ).to.equal( '<p>Bar</p><p>Biz</p>' );
  155. } );
  156. it( 'should return empty paragraph when body is empty', () => {
  157. setData( model, '<title>Foo</title>' );
  158. expect( editor.plugins.get( 'Title' ).getBody() ).to.equal( '<p>&nbsp;</p>' );
  159. } );
  160. } );
  161. describe( 'placeholders', () => {
  162. let viewRoot;
  163. beforeEach( () => {
  164. viewRoot = editor.editing.view.document.getRoot();
  165. } );
  166. it( 'should attach placeholder placeholder to title and body', () => {
  167. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  168. const title = viewRoot.getChild( 0 );
  169. const body = viewRoot.getChild( 1 );
  170. expect( title.getAttribute( 'data-placeholder' ) ).to.equal( 'Title' );
  171. expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'Body' );
  172. expect( title.hasClass( 'ck-placeholder' ) ).to.equal( false );
  173. expect( body.hasClass( 'ck-placeholder' ) ).to.equal( false );
  174. } );
  175. it( 'should show placeholder in empty title and body', () => {
  176. setData( model, '<title></title><paragraph></paragraph>' );
  177. const title = viewRoot.getChild( 0 );
  178. const body = viewRoot.getChild( 1 );
  179. expect( title.getAttribute( 'data-placeholder' ) ).to.equal( 'Title' );
  180. expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'Body' );
  181. expect( title.hasClass( 'ck-placeholder' ) ).to.equal( true );
  182. expect( body.hasClass( 'ck-placeholder' ) ).to.equal( true );
  183. } );
  184. it( 'should hide placeholder from body with more than one child elements', () => {
  185. setData( editor.model, '<title>Foo</title><paragraph></paragraph><paragraph></paragraph>' );
  186. const body = viewRoot.getChild( 1 );
  187. expect( body.getAttribute( 'data-placeholder' ) ).to.equal( 'Body' );
  188. expect( body.hasClass( 'ck-placeholder' ) ).to.equal( false );
  189. } );
  190. it( 'should hide placeholder from body with element other than paragraph', () => {
  191. setData( editor.model, '<title>Foo</title><heading1></heading1>' );
  192. const body = viewRoot.getChild( 1 );
  193. expect( body.hasAttribute( 'data-placeholder' ) ).to.equal( true );
  194. expect( body.hasClass( 'ck-placeholder' ) ).to.equal( false );
  195. } );
  196. it( 'should hide placeholder when title element become not empty', () => {
  197. setData( model, '<title></title><paragraph>[]</paragraph>' );
  198. expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.equal( true );
  199. model.change( writer => {
  200. writer.appendText( 'Bar', null, model.document.getRoot().getChild( 0 ) );
  201. } );
  202. expect( viewRoot.getChild( 0 ).hasClass( 'ck-placeholder' ) ).to.equal( false );
  203. } );
  204. it( 'should hide placeholder when body element become not empty', () => {
  205. setData( model, '<title>Foo</title><paragraph></paragraph>' );
  206. expect( viewRoot.getChild( 1 ).hasClass( 'ck-placeholder' ) ).to.equal( true );
  207. model.change( writer => {
  208. writer.appendText( 'Bar', null, model.document.getRoot().getChild( 1 ) );
  209. } );
  210. expect( viewRoot.getChild( 1 ).hasClass( 'ck-placeholder' ) ).to.equal( false );
  211. } );
  212. it( 'should properly map the body placeholder in DOM when undoing', () => {
  213. const viewRoot = editor.editing.view.document.getRoot();
  214. const domConverter = editor.editing.view.domConverter;
  215. let bodyDomElement;
  216. setData( editor.model, '<title>[Foo</title><paragraph>Bar</paragraph><paragraph>Baz]</paragraph>' );
  217. editor.model.deleteContent( editor.model.document.selection );
  218. bodyDomElement = domConverter.mapViewToDom( viewRoot.getChild( 1 ) );
  219. expect( bodyDomElement.dataset.placeholder ).to.equal( 'Body' );
  220. expect( bodyDomElement.classList.contains( 'ck-placeholder' ) ).to.be.true;
  221. editor.execute( 'undo' );
  222. bodyDomElement = domConverter.mapViewToDom( viewRoot.getChild( 1 ) );
  223. expect( bodyDomElement.dataset.placeholder ).to.equal( 'Body' );
  224. expect( bodyDomElement.classList.contains( 'ck-placeholder' ) ).to.be.false;
  225. } );
  226. } );
  227. describe( 'Tab press handling', () => {
  228. it( 'should handle tab key when the selection is at the beginning of the title', () => {
  229. setData( model, '<title>[]foo</title><paragraph>bar</paragraph>' );
  230. const eventData = getEventData( keyCodes.tab );
  231. editor.keystrokes.press( eventData );
  232. sinon.assert.calledOnce( eventData.preventDefault );
  233. sinon.assert.calledOnce( eventData.stopPropagation );
  234. expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[]bar</paragraph>' );
  235. } );
  236. it( 'should handle tab key when the selection is at the end of the title', () => {
  237. setData( model, '<title>foo[]</title><paragraph>bar</paragraph>' );
  238. const eventData = getEventData( keyCodes.tab );
  239. editor.keystrokes.press( eventData );
  240. sinon.assert.calledOnce( eventData.preventDefault );
  241. sinon.assert.calledOnce( eventData.stopPropagation );
  242. expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[]bar</paragraph>' );
  243. } );
  244. it( 'should not handle tab key when the selection is in the title and body', () => {
  245. setData( model, '<title>fo[o</title><paragraph>b]ar</paragraph>' );
  246. const eventData = getEventData( keyCodes.tab );
  247. editor.keystrokes.press( eventData );
  248. sinon.assert.notCalled( eventData.preventDefault );
  249. sinon.assert.notCalled( eventData.stopPropagation );
  250. expect( getData( model ) ).to.equal( '<title>fo[o</title><paragraph>b]ar</paragraph>' );
  251. } );
  252. it( 'should not handle tab key when the selection is in the body', () => {
  253. setData( model, '<title>foo</title><paragraph>[]bar</paragraph>' );
  254. const eventData = getEventData( keyCodes.tab );
  255. editor.keystrokes.press( eventData );
  256. sinon.assert.notCalled( eventData.preventDefault );
  257. sinon.assert.notCalled( eventData.stopPropagation );
  258. expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[]bar</paragraph>' );
  259. } );
  260. } );
  261. describe( 'Shift + Tab press handling', () => {
  262. it( 'should handle shift + tab keys when the selection is at the beginning of the body', () => {
  263. setData( model, '<title>foo</title><paragraph>[]bar</paragraph>' );
  264. const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
  265. editor.keystrokes.press( eventData );
  266. sinon.assert.calledOnce( eventData.preventDefault );
  267. sinon.assert.calledOnce( eventData.stopPropagation );
  268. expect( getData( model ) ).to.equal( '<title>[]foo</title><paragraph>bar</paragraph>' );
  269. } );
  270. it( 'should not handle shift + tab keys when the selection is not at the beginning of the body', () => {
  271. setData( model, '<title>foo</title><paragraph>b[]ar</paragraph>' );
  272. const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
  273. editor.keystrokes.press( eventData );
  274. sinon.assert.notCalled( eventData.preventDefault );
  275. sinon.assert.notCalled( eventData.stopPropagation );
  276. expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>b[]ar</paragraph>' );
  277. } );
  278. it( 'should not handle shift + tab keys when the selection is not collapsed', () => {
  279. setData( model, '<title>foo</title><paragraph>[b]ar</paragraph>' );
  280. const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
  281. editor.keystrokes.press( eventData );
  282. sinon.assert.notCalled( eventData.preventDefault );
  283. sinon.assert.notCalled( eventData.stopPropagation );
  284. expect( getData( model ) ).to.equal( '<title>foo</title><paragraph>[b]ar</paragraph>' );
  285. } );
  286. it( 'should not handle shift + tab keys when the selection is in the title', () => {
  287. setData( model, '<title>[]foo</title><paragraph>bar</paragraph>' );
  288. const eventData = getEventData( keyCodes.tab, { shiftKey: true } );
  289. editor.keystrokes.press( eventData );
  290. sinon.assert.notCalled( eventData.preventDefault );
  291. sinon.assert.notCalled( eventData.stopPropagation );
  292. expect( getData( model ) ).to.equal( '<title>[]foo</title><paragraph>bar</paragraph>' );
  293. } );
  294. } );
  295. describe( 'prevent extra paragraphing', () => {
  296. it( 'should remove the extra paragraph when pasting to the editor with body placeholder', () => {
  297. setData( model, '<title>[]</title>' );
  298. const dataTransferMock = {
  299. getData: type => {
  300. if ( type === 'text/html' ) {
  301. return '<h1>Title</h1><p>Body</p>';
  302. }
  303. },
  304. types: [],
  305. files: []
  306. };
  307. editor.editing.view.document.fire( 'paste', {
  308. dataTransfer: dataTransferMock,
  309. preventDefault() {}
  310. } );
  311. expect( getData( model ) ).to.equal( '<title>Title</title><paragraph>Body[]</paragraph>' );
  312. } );
  313. it( 'should not remove the extra paragraph when pasting to the editor with directly created body element', () => {
  314. setData( model, '<title>[]</title><paragraph></paragraph>' );
  315. const dataTransferMock = {
  316. getData: type => {
  317. if ( type === 'text/html' ) {
  318. return '<h1>Title</h1><p>Body</p>';
  319. }
  320. },
  321. types: [],
  322. files: []
  323. };
  324. editor.editing.view.document.fire( 'paste', {
  325. dataTransfer: dataTransferMock,
  326. preventDefault() {}
  327. } );
  328. expect( getData( model ) ).to.equal( '<title>Title</title><paragraph>Body[]</paragraph><paragraph></paragraph>' );
  329. } );
  330. it( 'should remove the extra paragraph when pressing enter in the title', () => {
  331. setData( model, '<title>fo[]o</title>' );
  332. editor.execute( 'enter' );
  333. expect( getData( model ) ).to.equal( '<title>fo</title><paragraph>[]o</paragraph>' );
  334. } );
  335. it( 'should not remove the extra paragraph when pressing enter in the title when body is created directly', () => {
  336. setData( model, '<title>fo[]o</title><paragraph></paragraph>' );
  337. editor.execute( 'enter' );
  338. expect( getData( model ) ).to.equal( '<title>fo</title><paragraph>[]o</paragraph><paragraph></paragraph>' );
  339. } );
  340. } );
  341. describe( 'upload to title - integration', () => {
  342. let file, fileRepository;
  343. beforeEach( () => {
  344. fileRepository = editor.plugins.get( 'FileRepository' );
  345. fileRepository.createUploadAdapter = newLoader => new UploadAdapterMock( newLoader );
  346. file = createNativeFileMock();
  347. } );
  348. it( 'should move image after the title when was dropped before it', () => {
  349. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  350. const dataTransfer = new DataTransfer( { files: [ file ], types: [ 'Files' ] } );
  351. const targetRange = model.createRange( model.createPositionAt( model.document.getRoot(), 0 ) );
  352. const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
  353. editor.editing.view.document.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
  354. const { id } = fileRepository.getLoader( file );
  355. expect( getData( model ) ).to.equal(
  356. '<title>Foo</title>' +
  357. `[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
  358. '<paragraph>Bar</paragraph>'
  359. );
  360. } );
  361. it( 'should move image after the title when was dropped to it', () => {
  362. setData( model, '<title>Foo</title><paragraph>Bar</paragraph>' );
  363. const dataTransfer = new DataTransfer( { files: [ file ], types: [ 'Files' ] } );
  364. const title = model.document.getRoot().getChild( 0 );
  365. const targetRange = model.createRange( model.createPositionAt( title, 1 ) );
  366. const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
  367. editor.editing.view.document.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
  368. const { id } = fileRepository.getLoader( file );
  369. expect( getData( model ) ).to.equal(
  370. '<title>Foo</title>' +
  371. `[<image uploadId="${ id }" uploadStatus="reading"></image>]` +
  372. '<paragraph>Bar</paragraph>'
  373. );
  374. } );
  375. } );
  376. } );
  377. function getEventData( keyCode, { shiftKey = false } = {} ) {
  378. return {
  379. keyCode,
  380. shiftKey,
  381. preventDefault: sinon.spy(),
  382. stopPropagation: sinon.spy()
  383. };
  384. }