8
0

liststylesui.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /* globals document */
  6. import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
  7. import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
  8. import BlockQuote from '@ckeditor/ckeditor5-block-quote/src/blockquote';
  9. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  10. import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
  11. import ListStyles from '../src/liststyles';
  12. import ListStylesUI from '../src/liststylesui';
  13. import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
  14. import bulletedListIcon from '../theme/icons/bulletedlist.svg';
  15. import numberedListIcon from '../theme/icons/numberedlist.svg';
  16. import listStyleDiscIcon from '../theme/icons/liststyledisc.svg';
  17. import listStyleCircleIcon from '../theme/icons/liststylecircle.svg';
  18. import listStyleSquareIcon from '../theme/icons/liststylesquare.svg';
  19. import listStyleDecimalIcon from '../theme/icons/liststyledecimal.svg';
  20. import listStyleDecimalWithLeadingZeroIcon from '../theme/icons/liststyledecimalleadingzero.svg';
  21. import listStyleLowerRomanIcon from '../theme/icons/liststylelowerroman.svg';
  22. import listStyleUpperRomanIcon from '../theme/icons/liststyleupperroman.svg';
  23. import listStyleLowerLatinIcon from '../theme/icons/liststylelowerlatin.svg';
  24. import listStyleUpperLatinIcon from '../theme/icons/liststyleupperlatin.svg';
  25. describe( 'ListStylesUI', () => {
  26. let editorElement, editor, model, listStylesCommand;
  27. beforeEach( () => {
  28. editorElement = document.createElement( 'div' );
  29. document.body.appendChild( editorElement );
  30. return ClassicTestEditor.create( editorElement, { plugins: [ Paragraph, BlockQuote, ListStyles ] } )
  31. .then( newEditor => {
  32. editor = newEditor;
  33. model = editor.model;
  34. listStylesCommand = editor.commands.get( 'listStyles' );
  35. } );
  36. } );
  37. afterEach( () => {
  38. editorElement.remove();
  39. return editor.destroy();
  40. } );
  41. it( 'should be named', () => {
  42. expect( ListStylesUI.pluginName ).to.equal( 'ListStylesUI' );
  43. } );
  44. it( 'should be loaded', () => {
  45. expect( editor.plugins.get( ListStylesUI ) ).to.be.instanceOf( ListStylesUI );
  46. } );
  47. describe( 'init()', () => {
  48. describe( 'bulleted list dropdown', () => {
  49. let bulletedListCommand, bulletedListDropdown;
  50. beforeEach( () => {
  51. bulletedListCommand = editor.commands.get( 'bulletedList' );
  52. bulletedListDropdown = editor.ui.componentFactory.create( 'bulletedList' );
  53. } );
  54. it( 'should registered as "bulletedList" in the component factory', () => {
  55. expect( bulletedListDropdown ).to.be.instanceOf( DropdownView );
  56. } );
  57. it( 'should have #isEnabled bound to the "bulletedList" command state', () => {
  58. expect( bulletedListDropdown.isEnabled ).to.be.true;
  59. bulletedListCommand.isEnabled = true;
  60. expect( bulletedListDropdown.isEnabled ).to.be.true;
  61. bulletedListCommand.isEnabled = false;
  62. expect( bulletedListDropdown.isEnabled ).to.be.false;
  63. } );
  64. it( 'should have a specific CSS class', () => {
  65. expect( bulletedListDropdown.class ).to.equal( 'ck-list-styles-dropdown' );
  66. } );
  67. describe( 'main split button', () => {
  68. let mainButtonView;
  69. beforeEach( () => {
  70. mainButtonView = bulletedListDropdown.buttonView;
  71. } );
  72. it( 'should have a #label', () => {
  73. expect( mainButtonView.label ).to.equal( 'Bulleted List' );
  74. } );
  75. it( 'should have an #icon', () => {
  76. expect( mainButtonView.icon ).to.equal( bulletedListIcon );
  77. } );
  78. it( 'should have a #tooltip based on a label', () => {
  79. expect( mainButtonView.tooltip ).to.be.true;
  80. } );
  81. it( 'should be toggleable', () => {
  82. expect( mainButtonView.isToggleable ).to.be.true;
  83. } );
  84. it( 'should have the #isOn state bound to the value of the "bulletedList" command', () => {
  85. expect( mainButtonView.isOn ).to.be.false;
  86. bulletedListCommand.value = 'foo';
  87. expect( mainButtonView.isOn ).to.be.true;
  88. bulletedListCommand.value = null;
  89. expect( mainButtonView.isOn ).to.be.false;
  90. } );
  91. it( 'should execute the "bulletedList" command and focus the editing view when clicked', () => {
  92. sinon.spy( editor, 'execute' );
  93. sinon.spy( editor.editing.view, 'focus' );
  94. mainButtonView.fire( 'execute' );
  95. sinon.assert.calledWithExactly( editor.execute, 'bulletedList' );
  96. sinon.assert.calledOnce( editor.editing.view.focus );
  97. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  98. } );
  99. } );
  100. describe( 'toolbar with style buttons', () => {
  101. let toolbarView;
  102. beforeEach( () => {
  103. toolbarView = bulletedListDropdown.toolbarView;
  104. } );
  105. it( 'should be in the dropdown panel', () => {
  106. expect( bulletedListDropdown.panelView.children.get( 0 ) ).to.equal( toolbarView );
  107. } );
  108. it( 'should have a proper ARIA label', () => {
  109. expect( toolbarView.ariaLabel ).to.equal( 'Bulleted list styles toolbar' );
  110. } );
  111. it( 'should bring the "disc" list style button', () => {
  112. const buttonView = toolbarView.items.get( 0 );
  113. expect( buttonView.label ).to.equal( 'Toggle the disc list style' );
  114. expect( buttonView.tooltip ).to.equal( 'Disc' );
  115. expect( buttonView.icon ).to.equal( listStyleDiscIcon );
  116. } );
  117. it( 'should bring the "circle" list style button', () => {
  118. const buttonView = toolbarView.items.get( 1 );
  119. expect( buttonView.label ).to.equal( 'Toggle the circle list style' );
  120. expect( buttonView.tooltip ).to.equal( 'Circle' );
  121. expect( buttonView.icon ).to.equal( listStyleCircleIcon );
  122. } );
  123. it( 'should bring the "square" list style button', () => {
  124. const buttonView = toolbarView.items.get( 2 );
  125. expect( buttonView.label ).to.equal( 'Toggle the square list style' );
  126. expect( buttonView.tooltip ).to.equal( 'Square' );
  127. expect( buttonView.icon ).to.equal( listStyleSquareIcon );
  128. } );
  129. describe( 'style button', () => {
  130. let styleButtonView;
  131. beforeEach( () => {
  132. // "circle"
  133. styleButtonView = toolbarView.items.get( 1 );
  134. sinon.spy( editor, 'execute' );
  135. sinon.spy( editor.editing.view, 'focus' );
  136. } );
  137. it( 'should be instances of ButtonView', () => {
  138. expect( styleButtonView ).to.be.instanceOf( ButtonView );
  139. } );
  140. it( 'should change its #isOn state when the value of the "listStylesCommand" command changes', () => {
  141. expect( styleButtonView.isOn ).to.be.false;
  142. listStylesCommand.value = 'foo';
  143. expect( styleButtonView.isOn ).to.be.false;
  144. listStylesCommand.value = 'circle';
  145. expect( styleButtonView.isOn ).to.be.true;
  146. listStylesCommand.value = null;
  147. expect( styleButtonView.isOn ).to.be.false;
  148. } );
  149. it( 'should apply the new style if none was set', () => {
  150. setData( model, '<listItem listType="bulleted" listIndent="0">[]foo</listItem>' );
  151. styleButtonView.fire( 'execute' );
  152. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'circle' } );
  153. sinon.assert.calledOnce( editor.editing.view.focus );
  154. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  155. } );
  156. it( 'should apply the new style if a different one was set', () => {
  157. setData( model, '<listItem listType="bulleted" listStyle="square" listIndent="0">[]foo</listItem>' );
  158. styleButtonView.fire( 'execute' );
  159. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'circle' } );
  160. sinon.assert.calledOnce( editor.editing.view.focus );
  161. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  162. } );
  163. it( 'should remove (toggle) the style if the same style was set', () => {
  164. setData( model, '<listItem listType="bulleted" listStyle="circle" listIndent="0">[]foo</listItem>' );
  165. styleButtonView.fire( 'execute' );
  166. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'default' } );
  167. sinon.assert.calledOnce( editor.editing.view.focus );
  168. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  169. } );
  170. it( 'should execute the "bulletedList" command and apply the style if selection was not anchored in a list', () => {
  171. setData( model, '<paragraph>foo[]</paragraph>' );
  172. styleButtonView.fire( 'execute' );
  173. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'circle' } );
  174. sinon.assert.calledOnce( editor.editing.view.focus );
  175. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  176. } );
  177. } );
  178. } );
  179. } );
  180. describe( 'numbered list dropdown', () => {
  181. let numberedListCommand, numberedListDropdown;
  182. beforeEach( () => {
  183. numberedListCommand = editor.commands.get( 'numberedList' );
  184. numberedListDropdown = editor.ui.componentFactory.create( 'numberedList' );
  185. } );
  186. it( 'should registered as "numberedList" in the component factory', () => {
  187. expect( numberedListDropdown ).to.be.instanceOf( DropdownView );
  188. } );
  189. it( 'should have #isEnabled bound to the "numberedList" command state', () => {
  190. expect( numberedListDropdown.isEnabled ).to.be.true;
  191. numberedListCommand.isEnabled = true;
  192. expect( numberedListDropdown.isEnabled ).to.be.true;
  193. numberedListCommand.isEnabled = false;
  194. expect( numberedListDropdown.isEnabled ).to.be.false;
  195. } );
  196. it( 'should have a specific CSS class', () => {
  197. expect( numberedListDropdown.class ).to.equal( 'ck-list-styles-dropdown' );
  198. } );
  199. describe( 'main split button', () => {
  200. let mainButtonView;
  201. beforeEach( () => {
  202. mainButtonView = numberedListDropdown.buttonView;
  203. } );
  204. it( 'should have a #label', () => {
  205. expect( mainButtonView.label ).to.equal( 'Numbered List' );
  206. } );
  207. it( 'should have an #icon', () => {
  208. expect( mainButtonView.icon ).to.equal( numberedListIcon );
  209. } );
  210. it( 'should have a #tooltip based on a label', () => {
  211. expect( mainButtonView.tooltip ).to.be.true;
  212. } );
  213. it( 'should be toggleable', () => {
  214. expect( mainButtonView.isToggleable ).to.be.true;
  215. } );
  216. it( 'should have the #isOn state bound to the value of the "numberedList" command', () => {
  217. expect( mainButtonView.isOn ).to.be.false;
  218. numberedListCommand.value = 'foo';
  219. expect( mainButtonView.isOn ).to.be.true;
  220. numberedListCommand.value = null;
  221. expect( mainButtonView.isOn ).to.be.false;
  222. } );
  223. it( 'should execute the "numberedList" command and focus the editing view when clicked', () => {
  224. sinon.spy( editor, 'execute' );
  225. sinon.spy( editor.editing.view, 'focus' );
  226. mainButtonView.fire( 'execute' );
  227. sinon.assert.calledWithExactly( editor.execute, 'numberedList' );
  228. sinon.assert.calledOnce( editor.editing.view.focus );
  229. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  230. } );
  231. } );
  232. describe( 'toolbar with style buttons', () => {
  233. let toolbarView;
  234. beforeEach( () => {
  235. toolbarView = numberedListDropdown.toolbarView;
  236. } );
  237. it( 'should be in the dropdown panel', () => {
  238. expect( numberedListDropdown.panelView.children.get( 0 ) ).to.equal( toolbarView );
  239. } );
  240. it( 'should have a proper ARIA label', () => {
  241. expect( toolbarView.ariaLabel ).to.equal( 'Numbered list styles toolbar' );
  242. } );
  243. it( 'should bring the "decimal" list style button', () => {
  244. const buttonView = toolbarView.items.get( 0 );
  245. expect( buttonView.label ).to.equal( 'Toggle the decimal list style' );
  246. expect( buttonView.tooltip ).to.equal( 'Decimal' );
  247. expect( buttonView.icon ).to.equal( listStyleDecimalIcon );
  248. } );
  249. it( 'should bring the "decimal-leading-zero" list style button', () => {
  250. const buttonView = toolbarView.items.get( 1 );
  251. expect( buttonView.label ).to.equal( 'Toggle the decimal with leading zero list style' );
  252. expect( buttonView.tooltip ).to.equal( 'Decimal with leading zero' );
  253. expect( buttonView.icon ).to.equal( listStyleDecimalWithLeadingZeroIcon );
  254. } );
  255. it( 'should bring the "lower-roman" list style button', () => {
  256. const buttonView = toolbarView.items.get( 2 );
  257. expect( buttonView.label ).to.equal( 'Toggle the lower–roman list style' );
  258. expect( buttonView.tooltip ).to.equal( 'Lower–roman' );
  259. expect( buttonView.icon ).to.equal( listStyleLowerRomanIcon );
  260. } );
  261. it( 'should bring the "upper-roman" list style button', () => {
  262. const buttonView = toolbarView.items.get( 3 );
  263. expect( buttonView.label ).to.equal( 'Toggle the upper–roman list style' );
  264. expect( buttonView.tooltip ).to.equal( 'Upper-roman' );
  265. expect( buttonView.icon ).to.equal( listStyleUpperRomanIcon );
  266. } );
  267. it( 'should bring the "lower–latin" list style button', () => {
  268. const buttonView = toolbarView.items.get( 4 );
  269. expect( buttonView.label ).to.equal( 'Toggle the lower–latin list style' );
  270. expect( buttonView.tooltip ).to.equal( 'Lower-latin' );
  271. expect( buttonView.icon ).to.equal( listStyleLowerLatinIcon );
  272. } );
  273. it( 'should bring the "upper–latin" list style button', () => {
  274. const buttonView = toolbarView.items.get( 5 );
  275. expect( buttonView.label ).to.equal( 'Toggle the upper–latin list style' );
  276. expect( buttonView.tooltip ).to.equal( 'Upper-latin' );
  277. expect( buttonView.icon ).to.equal( listStyleUpperLatinIcon );
  278. } );
  279. describe( 'style button', () => {
  280. let styleButtonView;
  281. beforeEach( () => {
  282. // "decimal-leading-zero""
  283. styleButtonView = toolbarView.items.get( 1 );
  284. sinon.spy( editor, 'execute' );
  285. sinon.spy( editor.editing.view, 'focus' );
  286. } );
  287. it( 'should be instances of ButtonView', () => {
  288. expect( styleButtonView ).to.be.instanceOf( ButtonView );
  289. } );
  290. it( 'should change its #isOn state when the value of the "listStylesCommand" command changes', () => {
  291. expect( styleButtonView.isOn ).to.be.false;
  292. listStylesCommand.value = 'foo';
  293. expect( styleButtonView.isOn ).to.be.false;
  294. listStylesCommand.value = 'decimal-leading-zero';
  295. expect( styleButtonView.isOn ).to.be.true;
  296. listStylesCommand.value = null;
  297. expect( styleButtonView.isOn ).to.be.false;
  298. } );
  299. it( 'should apply the new style if none was set', () => {
  300. setData( model, '<listItem listType="numbered" listIndent="0">[]foo</listItem>' );
  301. styleButtonView.fire( 'execute' );
  302. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'decimal-leading-zero' } );
  303. sinon.assert.calledOnce( editor.editing.view.focus );
  304. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  305. } );
  306. it( 'should apply the new style if a different one was set', () => {
  307. setData( model, '<listItem listType="numbered" listStyle="square" listIndent="0">[]foo</listItem>' );
  308. styleButtonView.fire( 'execute' );
  309. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'decimal-leading-zero' } );
  310. sinon.assert.calledOnce( editor.editing.view.focus );
  311. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  312. } );
  313. it( 'should remove (toggle) the style if the same style was set', () => {
  314. setData( model, '<listItem listType="numbered" listStyle="decimal-leading-zero" listIndent="0">[]foo</listItem>' );
  315. styleButtonView.fire( 'execute' );
  316. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'default' } );
  317. sinon.assert.calledOnce( editor.editing.view.focus );
  318. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  319. } );
  320. it( 'should execute the "numberedList" command and apply the style if selection was not anchored in a list', () => {
  321. setData( model, '<paragraph>foo[]</paragraph>' );
  322. styleButtonView.fire( 'execute' );
  323. sinon.assert.calledWithExactly( editor.execute, 'listStyles', { type: 'decimal-leading-zero' } );
  324. sinon.assert.calledOnce( editor.editing.view.focus );
  325. sinon.assert.callOrder( editor.execute, editor.editing.view.focus );
  326. } );
  327. } );
  328. } );
  329. } );
  330. } );
  331. } );