utils.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /* globals document, Event */
  6. import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
  7. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  8. import utilsTestUtils from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
  9. import Model from '../../src/model';
  10. import View from '../../src/view';
  11. import ButtonView from '../../src/button/buttonview';
  12. import ToolbarView from '../../src/toolbar/toolbarview';
  13. import ListItemView from '../../src/list/listitemview';
  14. import ListView from '../../src/list/listview';
  15. import DropdownView from '../../src/dropdown/dropdownview';
  16. import DropdownPanelView from '../../src/dropdown/dropdownpanelview';
  17. import SplitButtonView from '../../src/button/splitbuttonview';
  18. import {
  19. addListViewToDropdown,
  20. addToolbarToDropdown,
  21. closeDropdownOnBlur,
  22. closeDropdownOnExecute,
  23. createButtonForDropdown,
  24. createDropdownView,
  25. createSingleButtonDropdown,
  26. createSplitButtonForDropdown,
  27. enableModelIfOneIsEnabled,
  28. focusDropdownContentsOnArrows
  29. } from '../../src/dropdown/utils';
  30. const assertBinding = utilsTestUtils.assertBinding;
  31. describe( 'utils', () => {
  32. let dropdownView, buttonView, model, locale;
  33. beforeEach( () => {
  34. locale = { t() {} };
  35. model = new Model();
  36. buttonView = createButtonForDropdown( model, locale );
  37. dropdownView = createDropdownView( model, buttonView, locale );
  38. } );
  39. afterEach( () => {
  40. dropdownView.element.remove();
  41. } );
  42. describe( 'focusDropdownContentsOnArrows()', () => {
  43. let panelChildView;
  44. beforeEach( () => {
  45. panelChildView = new View();
  46. panelChildView.setTemplate( { tag: 'div' } );
  47. panelChildView.focus = () => {};
  48. panelChildView.focusLast = () => {};
  49. // TODO: describe this as #contentView instaed of #listView and #toolbarView
  50. dropdownView.panelView.children.add( panelChildView );
  51. focusDropdownContentsOnArrows( dropdownView );
  52. dropdownView.render();
  53. document.body.appendChild( dropdownView.element );
  54. } );
  55. it( '"arrowdown" focuses the #innerPanelView if dropdown is open', () => {
  56. const keyEvtData = {
  57. keyCode: keyCodes.arrowdown,
  58. preventDefault: sinon.spy(),
  59. stopPropagation: sinon.spy()
  60. };
  61. const spy = sinon.spy( panelChildView, 'focus' );
  62. dropdownView.isOpen = false;
  63. dropdownView.keystrokes.press( keyEvtData );
  64. sinon.assert.notCalled( spy );
  65. dropdownView.isOpen = true;
  66. dropdownView.keystrokes.press( keyEvtData );
  67. sinon.assert.calledOnce( spy );
  68. } );
  69. it( '"arrowup" focuses the last #item in #innerPanelView if dropdown is open', () => {
  70. const keyEvtData = {
  71. keyCode: keyCodes.arrowup,
  72. preventDefault: sinon.spy(),
  73. stopPropagation: sinon.spy()
  74. };
  75. const spy = sinon.spy( panelChildView, 'focusLast' );
  76. dropdownView.isOpen = false;
  77. dropdownView.keystrokes.press( keyEvtData );
  78. sinon.assert.notCalled( spy );
  79. dropdownView.isOpen = true;
  80. dropdownView.keystrokes.press( keyEvtData );
  81. sinon.assert.calledOnce( spy );
  82. } );
  83. } );
  84. describe( 'closeDropdownOnExecute()', () => {
  85. beforeEach( () => {
  86. closeDropdownOnExecute( dropdownView );
  87. dropdownView.render();
  88. document.body.appendChild( dropdownView.element );
  89. } );
  90. it( 'changes view#isOpen on view#execute', () => {
  91. dropdownView.isOpen = true;
  92. dropdownView.fire( 'execute' );
  93. expect( dropdownView.isOpen ).to.be.false;
  94. dropdownView.fire( 'execute' );
  95. expect( dropdownView.isOpen ).to.be.false;
  96. } );
  97. } );
  98. describe( 'closeDropdownOnBlur()', () => {
  99. beforeEach( () => {
  100. closeDropdownOnBlur( dropdownView );
  101. dropdownView.render();
  102. document.body.appendChild( dropdownView.element );
  103. } );
  104. it( 'listens to view#isOpen and reacts to DOM events (valid target)', () => {
  105. // Open the dropdown.
  106. dropdownView.isOpen = true;
  107. // Fire event from outside of the dropdown.
  108. document.body.dispatchEvent( new Event( 'mousedown', {
  109. bubbles: true
  110. } ) );
  111. // Closed the dropdown.
  112. expect( dropdownView.isOpen ).to.be.false;
  113. // Fire event from outside of the dropdown.
  114. document.body.dispatchEvent( new Event( 'mousedown', {
  115. bubbles: true
  116. } ) );
  117. // Dropdown is still closed.
  118. expect( dropdownView.isOpen ).to.be.false;
  119. } );
  120. it( 'listens to view#isOpen and reacts to DOM events (invalid target)', () => {
  121. // Open the dropdown.
  122. dropdownView.isOpen = true;
  123. // Event from view.element should be discarded.
  124. dropdownView.element.dispatchEvent( new Event( 'mousedown', {
  125. bubbles: true
  126. } ) );
  127. // Dropdown is still open.
  128. expect( dropdownView.isOpen ).to.be.true;
  129. // Event from within view.element should be discarded.
  130. const child = document.createElement( 'div' );
  131. dropdownView.element.appendChild( child );
  132. child.dispatchEvent( new Event( 'mousedown', {
  133. bubbles: true
  134. } ) );
  135. // Dropdown is still open.
  136. expect( dropdownView.isOpen ).to.be.true;
  137. } );
  138. } );
  139. describe( 'createButtonForDropdown()', () => {
  140. beforeEach( () => {
  141. buttonView = createButtonForDropdown( new Model(), locale );
  142. } );
  143. it( 'accepts locale', () => {
  144. expect( buttonView.locale ).to.equal( locale );
  145. } );
  146. it( 'returns ButtonView instance', () => {
  147. expect( buttonView ).to.be.instanceof( ButtonView );
  148. } );
  149. it( 'delegates "execute" to "select" event', () => {
  150. const spy = sinon.spy();
  151. buttonView.on( 'select', spy );
  152. buttonView.fire( 'exec' );
  153. sinon.assert.calledOnce( spy );
  154. } );
  155. } );
  156. describe( 'createSplitButtonForDropdown()', () => {
  157. beforeEach( () => {
  158. buttonView = createSplitButtonForDropdown( new Model(), locale );
  159. } );
  160. it( 'accepts locale', () => {
  161. expect( buttonView.locale ).to.equal( locale );
  162. } );
  163. it( 'returns SplitButtonView instance', () => {
  164. expect( buttonView ).to.be.instanceof( SplitButtonView );
  165. } );
  166. it( 'binds actionView "execute" to "select" event', () => {
  167. const spy = sinon.spy();
  168. buttonView.on( 'select', spy );
  169. buttonView.fire( 'exec' );
  170. sinon.assert.calledOnce( spy );
  171. } );
  172. } );
  173. describe( 'createDropdownView()', () => {
  174. it( 'returns view', () => {
  175. model = new Model();
  176. buttonView = new ButtonView();
  177. dropdownView = createDropdownView( model, buttonView, locale );
  178. expect( dropdownView ).to.be.instanceOf( DropdownView );
  179. } );
  180. it( 'creates dropdown#panelView out of DropdownPanelView', () => {
  181. model = new Model();
  182. buttonView = new ButtonView();
  183. dropdownView = createDropdownView( model, buttonView, locale );
  184. expect( dropdownView.panelView ).to.be.instanceOf( DropdownPanelView );
  185. } );
  186. it( 'creates dropdown#buttonView out of buttonView', () => {
  187. model = new Model();
  188. buttonView = new ButtonView();
  189. dropdownView = createDropdownView( model, buttonView, locale );
  190. expect( dropdownView.buttonView ).to.equal( buttonView );
  191. } );
  192. it( 'accepts locale', () => {
  193. const buttonView = new ButtonView();
  194. dropdownView = createDropdownView( model, buttonView, locale );
  195. expect( dropdownView.locale ).to.equal( locale );
  196. expect( dropdownView.panelView.locale ).to.equal( locale );
  197. } );
  198. it( 'binds button attributes to the model', () => {
  199. const modelDef = {
  200. label: 'foo',
  201. isOn: false,
  202. isEnabled: true,
  203. withText: false,
  204. tooltip: false
  205. };
  206. model = new Model( modelDef );
  207. buttonView = new ButtonView();
  208. createDropdownView( model, buttonView, locale );
  209. assertBinding( buttonView,
  210. modelDef,
  211. [
  212. [ model, { label: 'bar', isEnabled: false, isOn: true, withText: true, tooltip: true } ]
  213. ],
  214. { label: 'bar', isEnabled: false, isOn: true, withText: true, tooltip: true }
  215. );
  216. } );
  217. it( 'binds button#isOn do dropdown #isOpen and model #isOn', () => {
  218. const modelDef = {
  219. label: 'foo',
  220. isOn: false,
  221. isEnabled: true,
  222. withText: false,
  223. tooltip: false
  224. };
  225. model = new Model( modelDef );
  226. buttonView = new ButtonView();
  227. dropdownView = createDropdownView( model, buttonView, locale );
  228. dropdownView.isOpen = false;
  229. expect( buttonView.isOn ).to.be.false;
  230. model.isOn = true;
  231. expect( buttonView.isOn ).to.be.true;
  232. dropdownView.isOpen = true;
  233. expect( buttonView.isOn ).to.be.true;
  234. model.isOn = false;
  235. expect( buttonView.isOn ).to.be.true;
  236. } );
  237. it( 'binds dropdown#isEnabled to the model', () => {
  238. const modelDef = {
  239. label: 'foo',
  240. isEnabled: true,
  241. withText: false,
  242. tooltip: false
  243. };
  244. model = new Model( modelDef );
  245. buttonView = new ButtonView();
  246. dropdownView = createDropdownView( model, buttonView, locale );
  247. assertBinding( dropdownView,
  248. { isEnabled: true },
  249. [
  250. [ model, { isEnabled: false } ]
  251. ],
  252. { isEnabled: false }
  253. );
  254. } );
  255. } );
  256. describe( 'createSplitButtonDropdown()', () => { } );
  257. describe( 'createSingleButtonDropdown()', () => {} );
  258. describe( 'enableModelIfOneIsEnabled()', () => {
  259. it( 'Bind to #isEnabled of each observable and set it true if any observable #isEnabled is true', () => {
  260. const observables = [
  261. new Model( { isEnabled: false } ),
  262. new Model( { isEnabled: false } ),
  263. new Model( { isEnabled: false } )
  264. ];
  265. enableModelIfOneIsEnabled( model, observables );
  266. expect( model.isEnabled ).to.be.false;
  267. observables[ 0 ].isEnabled = true;
  268. expect( model.isEnabled ).to.be.true;
  269. observables[ 0 ].isEnabled = false;
  270. expect( model.isEnabled ).to.be.false;
  271. observables[ 1 ].isEnabled = true;
  272. expect( model.isEnabled ).to.be.true;
  273. } );
  274. } );
  275. describe( 'addListViewToDropdown()', () => {
  276. let items;
  277. beforeEach( () => {
  278. items = new Collection();
  279. model = new Model( {
  280. isEnabled: true,
  281. items,
  282. isOn: false,
  283. label: 'foo'
  284. } );
  285. dropdownView = createSingleButtonDropdown( model, locale );
  286. addListViewToDropdown( dropdownView, model, locale );
  287. dropdownView.render();
  288. document.body.appendChild( dropdownView.element );
  289. } );
  290. it( 'sets view#locale', () => {
  291. expect( dropdownView.locale ).to.equal( locale );
  292. } );
  293. describe( 'view#listView', () => {
  294. it( 'is created', () => {
  295. const panelChildren = dropdownView.panelView.children;
  296. expect( panelChildren ).to.have.length( 1 );
  297. expect( panelChildren.get( 0 ) ).to.equal( dropdownView.listView );
  298. expect( dropdownView.listView ).to.be.instanceof( ListView );
  299. } );
  300. it( 'is bound to model#items', () => {
  301. items.add( new Model( { label: 'a', style: 'b' } ) );
  302. items.add( new Model( { label: 'c', style: 'd' } ) );
  303. expect( dropdownView.listView.items ).to.have.length( 2 );
  304. expect( dropdownView.listView.items.get( 0 ) ).to.be.instanceOf( ListItemView );
  305. expect( dropdownView.listView.items.get( 1 ).label ).to.equal( 'c' );
  306. expect( dropdownView.listView.items.get( 1 ).style ).to.equal( 'd' );
  307. items.remove( 1 );
  308. expect( dropdownView.listView.items ).to.have.length( 1 );
  309. expect( dropdownView.listView.items.get( 0 ).label ).to.equal( 'a' );
  310. expect( dropdownView.listView.items.get( 0 ).style ).to.equal( 'b' );
  311. } );
  312. it( 'binds all attributes in model#items', () => {
  313. const itemModel = new Model( { label: 'a', style: 'b', foo: 'bar', baz: 'qux' } );
  314. items.add( itemModel );
  315. const item = dropdownView.listView.items.get( 0 );
  316. expect( item.foo ).to.equal( 'bar' );
  317. expect( item.baz ).to.equal( 'qux' );
  318. itemModel.baz = 'foo?';
  319. expect( item.baz ).to.equal( 'foo?' );
  320. } );
  321. it( 'delegates view.listView#execute to the view', done => {
  322. items.add( new Model( { label: 'a', style: 'b' } ) );
  323. dropdownView.on( 'execute', evt => {
  324. expect( evt.source ).to.equal( dropdownView.listView.items.get( 0 ) );
  325. expect( evt.path ).to.deep.equal( [ dropdownView.listView.items.get( 0 ), dropdownView ] );
  326. done();
  327. } );
  328. dropdownView.listView.items.get( 0 ).fire( 'execute' );
  329. } );
  330. } );
  331. } );
  332. describe( 'addToolbarToDropdown()', () => {
  333. let buttons;
  334. beforeEach( () => {
  335. buttons = [ '<svg>foo</svg>', '<svg>bar</svg>' ].map( icon => {
  336. const button = new ButtonView();
  337. button.icon = icon;
  338. return button;
  339. } );
  340. model = new Model( {
  341. isVertical: true,
  342. buttons
  343. } );
  344. dropdownView = createSingleButtonDropdown( model, locale );
  345. addToolbarToDropdown( dropdownView, model );
  346. dropdownView.render();
  347. document.body.appendChild( dropdownView.element );
  348. } );
  349. it( 'sets view#locale', () => {
  350. expect( dropdownView.locale ).to.equal( locale );
  351. } );
  352. it( 'sets view class', () => {
  353. expect( dropdownView.element.classList.contains( 'ck-toolbar-dropdown' ) ).to.be.true;
  354. } );
  355. describe( 'view#toolbarView', () => {
  356. it( 'is created', () => {
  357. const panelChildren = dropdownView.panelView.children;
  358. expect( panelChildren ).to.have.length( 1 );
  359. expect( panelChildren.get( 0 ) ).to.equal( dropdownView.toolbarView );
  360. expect( dropdownView.toolbarView ).to.be.instanceof( ToolbarView );
  361. } );
  362. it.skip( 'delegates view.toolbarView.items#execute to the view', done => {
  363. dropdownView.on( 'execute', evt => {
  364. expect( evt.source ).to.equal( dropdownView.toolbarView.items.get( 0 ) );
  365. expect( evt.path ).to.deep.equal( [ dropdownView.toolbarView.items.get( 0 ), dropdownView ] );
  366. done();
  367. } );
  368. dropdownView.toolbarView.items.get( 0 ).fire( 'execute' );
  369. } );
  370. it( 'reacts on model#isVertical', () => {
  371. model.isVertical = false;
  372. expect( dropdownView.toolbarView.isVertical ).to.be.false;
  373. model.isVertical = true;
  374. expect( dropdownView.toolbarView.isVertical ).to.be.true;
  375. } );
  376. } );
  377. describe( 'buttons', () => {
  378. // TODO: test me!
  379. } );
  380. } );
  381. describe( 'getBindingTargets()', () => {} );
  382. } );