downcastdispatcher.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  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. import DowncastDispatcher from '../../src/conversion/downcastdispatcher';
  6. import Mapper from '../../src/conversion/mapper';
  7. import Model from '../../src/model/model';
  8. import ModelText from '../../src/model/text';
  9. import ModelElement from '../../src/model/element';
  10. import ModelRange from '../../src/model/range';
  11. import View from '../../src/view/view';
  12. import ViewContainerElement from '../../src/view/containerelement';
  13. import { StylesProcessor } from '../../src/view/stylesmap';
  14. describe( 'DowncastDispatcher', () => {
  15. let dispatcher, doc, root, differStub, model, view, mapper;
  16. beforeEach( () => {
  17. model = new Model();
  18. view = new View( new StylesProcessor() );
  19. doc = model.document;
  20. mapper = new Mapper();
  21. dispatcher = new DowncastDispatcher( { mapper } );
  22. root = doc.createRoot();
  23. differStub = {
  24. getMarkersToRemove: () => [],
  25. getChanges: () => [],
  26. getMarkersToAdd: () => []
  27. };
  28. } );
  29. describe( 'constructor()', () => {
  30. it( 'should create DowncastDispatcher with given api', () => {
  31. const apiObj = {};
  32. const dispatcher = new DowncastDispatcher( { apiObj } );
  33. expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj );
  34. } );
  35. } );
  36. describe( 'convertChanges', () => {
  37. it( 'should call convertInsert for insert change', () => {
  38. sinon.stub( dispatcher, 'convertInsert' );
  39. const position = model.createPositionFromPath( root, [ 0 ] );
  40. const range = ModelRange._createFromPositionAndShift( position, 1 );
  41. differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ];
  42. view.change( writer => {
  43. dispatcher.convertChanges( differStub, model.markers, writer );
  44. } );
  45. expect( dispatcher.convertInsert.calledOnce ).to.be.true;
  46. expect( dispatcher.convertInsert.firstCall.args[ 0 ].isEqual( range ) ).to.be.true;
  47. } );
  48. it( 'should call convertRemove for remove change', () => {
  49. sinon.stub( dispatcher, 'convertRemove' );
  50. const position = model.createPositionFromPath( root, [ 0 ] );
  51. differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ];
  52. view.change( writer => {
  53. dispatcher.convertChanges( differStub, model.markers, writer );
  54. } );
  55. expect( dispatcher.convertRemove.calledWith( position, 2, '$text' ) ).to.be.true;
  56. } );
  57. it( 'should call convertAttribute for attribute change', () => {
  58. sinon.stub( dispatcher, 'convertAttribute' );
  59. const position = model.createPositionFromPath( root, [ 0 ] );
  60. const range = ModelRange._createFromPositionAndShift( position, 1 );
  61. differStub.getChanges = () => [
  62. { type: 'attribute', position, range, attributeKey: 'key', attributeOldValue: null, attributeNewValue: 'foo' }
  63. ];
  64. view.change( writer => {
  65. dispatcher.convertChanges( differStub, model.markers, writer );
  66. } );
  67. expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true;
  68. } );
  69. it( 'should handle multiple changes', () => {
  70. sinon.stub( dispatcher, 'convertInsert' );
  71. sinon.stub( dispatcher, 'convertRemove' );
  72. sinon.stub( dispatcher, 'convertAttribute' );
  73. const position = model.createPositionFromPath( root, [ 0 ] );
  74. const range = ModelRange._createFromPositionAndShift( position, 1 );
  75. differStub.getChanges = () => [
  76. { type: 'insert', position, length: 1 },
  77. { type: 'attribute', position, range, attributeKey: 'key', attributeOldValue: null, attributeNewValue: 'foo' },
  78. { type: 'remove', position, length: 1, name: 'paragraph' },
  79. { type: 'insert', position, length: 3 }
  80. ];
  81. view.change( writer => {
  82. dispatcher.convertChanges( differStub, model.markers, writer );
  83. } );
  84. expect( dispatcher.convertInsert.calledTwice ).to.be.true;
  85. expect( dispatcher.convertRemove.calledOnce ).to.be.true;
  86. expect( dispatcher.convertAttribute.calledOnce ).to.be.true;
  87. } );
  88. it( 'should call convertMarkerAdd when markers are added', () => {
  89. sinon.stub( dispatcher, 'convertMarkerAdd' );
  90. const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
  91. const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
  92. differStub.getMarkersToAdd = () => [
  93. { name: 'foo', range: fooRange },
  94. { name: 'bar', range: barRange }
  95. ];
  96. view.change( writer => {
  97. dispatcher.convertChanges( differStub, model.markers, writer );
  98. } );
  99. expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
  100. expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
  101. } );
  102. it( 'should call convertMarkerRemove when markers are removed', () => {
  103. sinon.stub( dispatcher, 'convertMarkerRemove' );
  104. const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
  105. const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
  106. differStub.getMarkersToRemove = () => [
  107. { name: 'foo', range: fooRange },
  108. { name: 'bar', range: barRange }
  109. ];
  110. view.change( writer => {
  111. dispatcher.convertChanges( differStub, model.markers, writer );
  112. } );
  113. expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
  114. expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
  115. } );
  116. it( 'should re-render markers which view elements got unbound during conversion', () => {
  117. sinon.stub( dispatcher, 'convertMarkerRemove' );
  118. sinon.stub( dispatcher, 'convertMarkerAdd' );
  119. const fooRange = model.createRange( model.createPositionAt( root, 0 ), model.createPositionAt( root, 1 ) );
  120. const barRange = model.createRange( model.createPositionAt( root, 3 ), model.createPositionAt( root, 6 ) );
  121. model.markers._set( 'foo', fooRange );
  122. model.markers._set( 'bar', barRange );
  123. // Stub `Mapper#flushUnboundMarkerNames`.
  124. dispatcher.conversionApi.mapper.flushUnboundMarkerNames = () => [ 'foo', 'bar' ];
  125. view.change( writer => {
  126. dispatcher.convertChanges( differStub, model.markers, writer );
  127. } );
  128. expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) );
  129. expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) );
  130. expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) );
  131. expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) );
  132. } );
  133. } );
  134. describe( 'convertInsert', () => {
  135. it( 'should fire event with correct parameters for every item in passed range', () => {
  136. root._appendChild( [
  137. new ModelText( 'foo', { bold: true } ),
  138. new ModelElement( 'image' ),
  139. new ModelText( 'bar' ),
  140. new ModelElement( 'paragraph', { class: 'nice' }, new ModelText( 'xx', { italic: true } ) )
  141. ] );
  142. const range = model.createRangeIn( root );
  143. const loggedEvents = [];
  144. // We will check everything connected with insert event:
  145. dispatcher.on( 'insert', ( evt, data, conversionApi ) => {
  146. // Check if the item is correct.
  147. const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
  148. // Check if the range is correct.
  149. const log = 'insert:' + itemId + ':' + data.range.start.path + ':' + data.range.end.path;
  150. loggedEvents.push( log );
  151. // Check if the event name is correct.
  152. expect( evt.name ).to.equal( 'insert:' + ( data.item.name || '$text' ) );
  153. // Check if model consumable is correct.
  154. expect( conversionApi.consumable.consume( data.item, 'insert' ) ).to.be.true;
  155. } );
  156. // Same here.
  157. dispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
  158. const itemId = data.item.name ? data.item.name : '$text:' + data.item.data;
  159. const key = data.attributeKey;
  160. const value = data.attributeNewValue;
  161. const log = 'attribute:' + key + ':' + value + ':' + itemId + ':' + data.range.start.path + ':' + data.range.end.path;
  162. loggedEvents.push( log );
  163. expect( evt.name ).to.equal( 'attribute:' + key + ':' + ( data.item.name || '$text' ) );
  164. expect( conversionApi.consumable.consume( data.item, 'attribute:' + key ) ).to.be.true;
  165. } );
  166. dispatcher.convertInsert( range );
  167. // Check the data passed to called events and the order of them.
  168. expect( loggedEvents ).to.deep.equal( [
  169. 'insert:$text:foo:0:3',
  170. 'attribute:bold:true:$text:foo:0:3',
  171. 'insert:image:3:4',
  172. 'insert:$text:bar:4:7',
  173. 'insert:paragraph:7:8',
  174. 'attribute:class:nice:paragraph:7:8',
  175. 'insert:$text:xx:7,0:7,2',
  176. 'attribute:italic:true:$text:xx:7,0:7,2'
  177. ] );
  178. } );
  179. it( 'should not fire events for already consumed parts of model', () => {
  180. root._appendChild( [
  181. new ModelElement( 'image', { src: 'foo.jpg', title: 'bar', bold: true }, [
  182. new ModelElement( 'caption', {}, new ModelText( 'title' ) )
  183. ] )
  184. ] );
  185. sinon.spy( dispatcher, 'fire' );
  186. dispatcher.on( 'insert:image', ( evt, data, conversionApi ) => {
  187. conversionApi.consumable.consume( data.item.getChild( 0 ), 'insert' );
  188. conversionApi.consumable.consume( data.item, 'attribute:bold' );
  189. } );
  190. const range = model.createRangeIn( root );
  191. dispatcher.convertInsert( range );
  192. expect( dispatcher.fire.calledWith( 'insert:image' ) ).to.be.true;
  193. expect( dispatcher.fire.calledWith( 'attribute:src:image' ) ).to.be.true;
  194. expect( dispatcher.fire.calledWith( 'attribute:title:image' ) ).to.be.true;
  195. expect( dispatcher.fire.calledWith( 'insert:$text' ) ).to.be.true;
  196. expect( dispatcher.fire.calledWith( 'attribute:bold:image' ) ).to.be.false;
  197. expect( dispatcher.fire.calledWith( 'insert:caption' ) ).to.be.false;
  198. } );
  199. } );
  200. describe( 'convertRemove', () => {
  201. it( 'should fire event for removed range', () => {
  202. const loggedEvents = [];
  203. dispatcher.on( 'remove:$text', ( evt, data ) => {
  204. const log = 'remove:' + data.position.path + ':' + data.length;
  205. loggedEvents.push( log );
  206. } );
  207. dispatcher.convertRemove( model.createPositionAt( root, 3 ), 3, '$text' );
  208. expect( loggedEvents ).to.deep.equal( [ 'remove:3:3' ] );
  209. } );
  210. } );
  211. describe( 'convertSelection', () => {
  212. beforeEach( () => {
  213. dispatcher.off( 'selection' );
  214. root._appendChild( new ModelText( 'foobar' ) );
  215. model.change( writer => {
  216. writer.setSelection( [
  217. writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 3 ] ) ),
  218. writer.createRange( writer.createPositionFromPath( root, [ 4 ] ), writer.createPositionFromPath( root, [ 5 ] ) )
  219. ] );
  220. } );
  221. } );
  222. it( 'should fire selection event', () => {
  223. sinon.spy( dispatcher, 'fire' );
  224. dispatcher.convertSelection( doc.selection, model.markers, [] );
  225. expect( dispatcher.fire.calledWith(
  226. 'selection',
  227. { selection: sinon.match.instanceOf( doc.selection.constructor ) }
  228. ) ).to.be.true;
  229. } );
  230. it( 'should prepare correct list of consumable values', () => {
  231. model.change( writer => {
  232. writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  233. writer.setAttribute( 'italic', true,
  234. writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
  235. );
  236. } );
  237. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  238. expect( conversionApi.consumable.test( data.selection, 'selection' ) ).to.be.true;
  239. expect( conversionApi.consumable.test( data.selection, 'attribute:bold' ) ).to.be.true;
  240. expect( conversionApi.consumable.test( data.selection, 'attribute:italic' ) ).to.be.null;
  241. } );
  242. dispatcher.convertSelection( doc.selection, model.markers, [] );
  243. } );
  244. it( 'should not fire attributes events for non-collapsed selection', () => {
  245. model.change( writer => {
  246. writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  247. writer.setAttribute( 'italic', true,
  248. writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
  249. );
  250. } );
  251. sinon.spy( dispatcher, 'fire' );
  252. dispatcher.convertSelection( doc.selection, model.markers, [] );
  253. expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false;
  254. expect( dispatcher.fire.calledWith( 'attribute:italic' ) ).to.be.false;
  255. } );
  256. it( 'should fire attributes events for collapsed selection', () => {
  257. model.change( writer => {
  258. writer.setSelection(
  259. writer.createRange( writer.createPositionFromPath( root, [ 2 ] ), writer.createPositionFromPath( root, [ 2 ] ) )
  260. );
  261. } );
  262. model.change( writer => {
  263. writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  264. } );
  265. sinon.spy( dispatcher, 'fire' );
  266. dispatcher.convertSelection( doc.selection, model.markers, [] );
  267. expect( dispatcher.fire.calledWith( 'attribute:bold:$text' ) ).to.be.true;
  268. } );
  269. it( 'should not fire attributes events if attribute has been consumed', () => {
  270. model.change( writer => {
  271. writer.setSelection(
  272. writer.createRange( writer.createPositionFromPath( root, [ 2 ] ), writer.createPositionFromPath( root, [ 2 ] ) )
  273. );
  274. } );
  275. model.change( writer => {
  276. writer.setAttribute( 'bold', true, writer.createRangeIn( root ) );
  277. writer.setAttribute( 'italic', true,
  278. writer.createRange( writer.createPositionAt( root, 4 ), writer.createPositionAt( root, 5 ) )
  279. );
  280. } );
  281. dispatcher.on( 'selection', ( evt, data, conversionApi ) => {
  282. conversionApi.consumable.consume( data.selection, 'attribute:bold' );
  283. } );
  284. sinon.spy( dispatcher, 'fire' );
  285. dispatcher.convertSelection( doc.selection, model.markers, [] );
  286. expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false;
  287. } );
  288. it( 'should fire events for markers for collapsed selection', () => {
  289. model.change( writer => {
  290. writer.setSelection(
  291. writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 1 ] ) )
  292. );
  293. const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
  294. writer.addMarker( 'name', { range, usingOperation: false } );
  295. } );
  296. sinon.spy( dispatcher, 'fire' );
  297. const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  298. dispatcher.convertSelection( doc.selection, model.markers, markers );
  299. expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.true;
  300. } );
  301. it( 'should not fire events for markers for non-collapsed selection', () => {
  302. model.change( writer => {
  303. const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
  304. writer.addMarker( 'name', { range, usingOperation: false } );
  305. } );
  306. sinon.spy( dispatcher, 'fire' );
  307. const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  308. dispatcher.convertSelection( doc.selection, model.markers, markers );
  309. expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
  310. } );
  311. it( 'should not fire event for marker if selection is in a element with custom highlight handling', () => {
  312. // Clear after `beforeEach`.
  313. root._removeChildren( 0, root.childCount );
  314. const text = new ModelText( 'abc' );
  315. const caption = new ModelElement( 'caption', null, text );
  316. const image = new ModelElement( 'image', null, caption );
  317. root._appendChild( [ image ] );
  318. // Create view elements that will be "mapped" to model elements.
  319. const viewCaption = new ViewContainerElement( view.document, 'caption' );
  320. const viewFigure = new ViewContainerElement( view.document, 'figure', null, viewCaption );
  321. // Create custom highlight handler mock.
  322. viewFigure._setCustomProperty( 'addHighlight', () => {} );
  323. viewFigure._setCustomProperty( 'removeHighlight', () => {} );
  324. // Create mapper mock.
  325. dispatcher.conversionApi.mapper = {
  326. toViewElement( modelElement ) {
  327. if ( modelElement == image ) {
  328. return viewFigure;
  329. } else if ( modelElement == caption ) {
  330. return viewCaption;
  331. }
  332. }
  333. };
  334. model.change( writer => {
  335. const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 1 ) );
  336. writer.addMarker( 'name', { range, usingOperation: false } );
  337. writer.setSelection( caption, 1 );
  338. } );
  339. sinon.spy( dispatcher, 'fire' );
  340. const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  341. dispatcher.convertSelection( doc.selection, model.markers, markers );
  342. expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false;
  343. } );
  344. it( 'should not fire events if information about marker has been consumed', () => {
  345. model.change( writer => {
  346. writer.setSelection(
  347. writer.createRange( writer.createPositionFromPath( root, [ 1 ] ), writer.createPositionFromPath( root, [ 1 ] ) )
  348. );
  349. const range = writer.createRange( writer.createPositionAt( root, 0 ), writer.createPositionAt( root, 2 ) );
  350. writer.addMarker( 'foo', { range, usingOperation: false } );
  351. writer.addMarker( 'bar', { range, usingOperation: false } );
  352. } );
  353. sinon.spy( dispatcher, 'fire' );
  354. dispatcher.on( 'addMarker:foo', ( evt, data, conversionApi ) => {
  355. conversionApi.consumable.consume( data.item, 'addMarker:bar' );
  356. } );
  357. const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) );
  358. dispatcher.convertSelection( doc.selection, model.markers, markers );
  359. expect( dispatcher.fire.calledWith( 'addMarker:foo' ) ).to.be.true;
  360. expect( dispatcher.fire.calledWith( 'addMarker:bar' ) ).to.be.false;
  361. } );
  362. } );
  363. describe( 'convertMarkerAdd', () => {
  364. let element, text;
  365. beforeEach( () => {
  366. text = new ModelText( 'foo bar baz' );
  367. element = new ModelElement( 'paragraph', null, [ text ] );
  368. root._appendChild( [ element ] );
  369. } );
  370. it( 'should fire addMarker event for whole collapsed marker', () => {
  371. const range = model.createRange( model.createPositionAt( element, 2 ), model.createPositionAt( element, 2 ) );
  372. const spy = sinon.spy();
  373. dispatcher.on( 'addMarker:name', ( evt, data ) => {
  374. spy();
  375. expect( data.markerName ).to.equal( 'name' );
  376. expect( data.markerRange.isEqual( range ) ).to.be.true;
  377. } );
  378. dispatcher.convertMarkerAdd( 'name', range );
  379. expect( spy.calledOnce ).to.be.true;
  380. } );
  381. it( 'should not convert marker if it is in graveyard', () => {
  382. const gyRange = model.createRange( model.createPositionAt( doc.graveyard, 0 ), model.createPositionAt( doc.graveyard, 0 ) );
  383. sinon.spy( dispatcher, 'fire' );
  384. dispatcher.convertMarkerAdd( 'name', gyRange );
  385. expect( dispatcher.fire.called ).to.be.false;
  386. } );
  387. it( 'should not convert marker if it is not in model root', () => {
  388. const element = new ModelElement( 'element', null, new ModelText( 'foo' ) );
  389. const eleRange = model.createRange( model.createPositionAt( element, 1 ), model.createPositionAt( element, 2 ) );
  390. sinon.spy( dispatcher, 'fire' );
  391. dispatcher.convertMarkerAdd( 'name', eleRange );
  392. expect( dispatcher.fire.called ).to.be.false;
  393. } );
  394. it( 'should fire addMarker event for whole non-collapsed marker and for each item in the range', () => {
  395. const range = model.createRangeIn( root );
  396. const spyWholeRange = sinon.spy();
  397. dispatcher.on( 'addMarker:name', ( evt, data ) => {
  398. if ( !data.item ) {
  399. spyWholeRange();
  400. expect( data.markerName ).to.equal( 'name' );
  401. expect( data.markerRange.isEqual( range ) ).to.be.true;
  402. }
  403. } );
  404. const spyItems = sinon.spy();
  405. const items = [];
  406. dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => {
  407. if ( data.item ) {
  408. spyItems();
  409. expect( data.markerName ).to.equal( 'name' );
  410. expect( data.markerRange.isEqual( range ) ).to.be.true;
  411. expect( conversionApi.consumable.test( data.item, 'addMarker:name' ) );
  412. items.push( data.item );
  413. }
  414. } );
  415. dispatcher.convertMarkerAdd( 'name', range );
  416. expect( spyWholeRange.calledOnce ).to.be.true;
  417. expect( spyItems.calledTwice ).to.be.true;
  418. expect( items[ 0 ] ).to.equal( element );
  419. expect( items[ 1 ].data ).to.equal( text.data );
  420. } );
  421. it( 'should not fire conversion for non-collapsed marker items if marker was consumed in earlier event', () => {
  422. const range = model.createRangeIn( root );
  423. dispatcher.on( 'addMarker:name', ( evt, data, conversionApi ) => {
  424. if ( !data.item ) {
  425. conversionApi.consumable.consume( data.markerRange, evt.name );
  426. }
  427. }, { priority: 'high' } );
  428. const spyItems = sinon.spy();
  429. dispatcher.on( 'addMarker:name', ( evt, data ) => {
  430. if ( data.item ) {
  431. spyItems();
  432. }
  433. } );
  434. dispatcher.convertMarkerAdd( 'name', range );
  435. expect( spyItems.called ).to.be.false;
  436. } );
  437. it( 'should be possible to override #1', () => {
  438. const range = model.createRangeIn( root );
  439. const addMarkerSpy = sinon.spy();
  440. const highAddMarkerSpy = sinon.spy();
  441. dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  442. if ( !data.item ) {
  443. addMarkerSpy();
  444. }
  445. } );
  446. dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  447. if ( !data.item ) {
  448. highAddMarkerSpy();
  449. evt.stop();
  450. }
  451. }, { priority: 'high' } );
  452. dispatcher.convertMarkerAdd( 'marker', range );
  453. expect( addMarkerSpy.called ).to.be.false;
  454. expect( highAddMarkerSpy.calledOnce ).to.be.true;
  455. } );
  456. it( 'should be possible to override #2', () => {
  457. const range = model.createRangeIn( root );
  458. const addMarkerSpy = sinon.spy();
  459. const highAddMarkerSpy = sinon.spy();
  460. dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  461. if ( data.item ) {
  462. addMarkerSpy();
  463. }
  464. } );
  465. dispatcher.on( 'addMarker:marker', ( evt, data ) => {
  466. if ( data.item ) {
  467. highAddMarkerSpy();
  468. evt.stop();
  469. }
  470. }, { priority: 'high' } );
  471. dispatcher.convertMarkerAdd( 'marker', range );
  472. expect( addMarkerSpy.called ).to.be.false;
  473. // Called once for each item, twice total.
  474. expect( highAddMarkerSpy.calledTwice ).to.be.true;
  475. } );
  476. } );
  477. describe( 'convertMarkerRemove', () => {
  478. let range, element, text;
  479. beforeEach( () => {
  480. text = new ModelText( 'foo bar baz' );
  481. element = new ModelElement( 'paragraph', null, [ text ] );
  482. root._appendChild( [ element ] );
  483. range = model.createRange( model.createPositionAt( element, 0 ), model.createPositionAt( element, 4 ) );
  484. } );
  485. it( 'should fire removeMarker event', () => {
  486. sinon.spy( dispatcher, 'fire' );
  487. dispatcher.convertMarkerRemove( 'name', range );
  488. expect( dispatcher.fire.calledWith( 'removeMarker:name' ) ).to.be.true;
  489. } );
  490. it( 'should not convert marker if it is in graveyard', () => {
  491. const gyRange = model.createRange( model.createPositionAt( doc.graveyard, 0 ), model.createPositionAt( doc.graveyard, 0 ) );
  492. sinon.spy( dispatcher, 'fire' );
  493. dispatcher.convertMarkerRemove( 'name', gyRange );
  494. expect( dispatcher.fire.called ).to.be.false;
  495. } );
  496. it( 'should not convert marker if it is not in model root', () => {
  497. const element = new ModelElement( 'element', null, new ModelText( 'foo' ) );
  498. const eleRange = model.createRange( model.createPositionAt( element, 1 ), model.createPositionAt( element, 2 ) );
  499. sinon.spy( dispatcher, 'fire' );
  500. dispatcher.convertMarkerRemove( 'name', eleRange );
  501. expect( dispatcher.fire.called ).to.be.false;
  502. } );
  503. it( 'should fire conversion for the range', () => {
  504. range = model.createRangeIn( root );
  505. dispatcher.on( 'addMarker:name', ( evt, data ) => {
  506. expect( data.markerName ).to.equal( 'name' );
  507. expect( data.markerRange.isEqual( range ) ).to.be.true;
  508. } );
  509. dispatcher.convertMarkerRemove( 'name', range );
  510. } );
  511. it( 'should be possible to override', () => {
  512. range = model.createRangeIn( root );
  513. const removeMarkerSpy = sinon.spy();
  514. const highRemoveMarkerSpy = sinon.spy();
  515. dispatcher.on( 'removeMarker:marker', removeMarkerSpy );
  516. dispatcher.on( 'removeMarker:marker', evt => {
  517. highRemoveMarkerSpy();
  518. evt.stop();
  519. }, { priority: 'high' } );
  520. dispatcher.convertMarkerRemove( 'marker', range );
  521. expect( removeMarkerSpy.called ).to.be.false;
  522. expect( highRemoveMarkerSpy.calledOnce ).to.be.true;
  523. } );
  524. } );
  525. } );