modelconversiondispatcher.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. /**
  2. * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import Consumable from './modelconsumable.js';
  6. import Range from '../model/range.js';
  7. import TextProxy from '../model/textproxy.js';
  8. import EmitterMixin from '../../utils/emittermixin.js';
  9. import mix from '../../utils/mix.js';
  10. import extend from '../../utils/lib/lodash/extend.js';
  11. /**
  12. * `ModelConversionDispatcher` is a central point of {@link engine.model model} conversion, which is
  13. * a process of reacting to changes in the model and reflecting them by listeners that listen to those changes.
  14. * In default application, {@link engine.model model} is converted to {@link engine.view view}. This means
  15. * that changes in the model are reflected by changing the view (i.e. adding view nodes or changing attributes on view elements).
  16. *
  17. * During conversion process, `ModelConversionDispatcher` fires data-manipulation events, basing on state of the model and prepares
  18. * data for those events. It is important to note that the events are connected with "change actions" like "inserting"
  19. * or "removing" so one might say that we are converting "changes". This is in contrary to view to model conversion,
  20. * where we convert view nodes (the structure, not "changes" to the view). Note, that because changes are converted
  21. * and not the structure itself, there is a need to have a mapping between model and the structure on which changes are
  22. * reflected. To map elements during model to view conversion use {@link engine.conversion.Mapper}.
  23. *
  24. * The main use for this class is to listen to {@link engine.model.Document.change Document change event}, process it
  25. * and then fire specific events telling what exactly has changed. For those events, `ModelConversionDispatcher`
  26. * creates {@link engine.conversion.ModelConsumable list of consumable values} that should be handled by event
  27. * callbacks. Those events are listened to by model-to-view converters which convert changes done in the
  28. * {@link engine.model model} to changes in the {@link engine.view view}. `ModelConversionController` also checks
  29. * the current state of consumables, so it won't fire events for parts of model that were already consumed. This is
  30. * especially important in callbacks that consume multiple values. See {@link engine.conversion.ModelConsumable}
  31. * for an example of such callback.
  32. *
  33. * Although the primary usage for this class is the model-to-view conversion, `ModelConversionDispatcher` can be used
  34. * to build custom data processing pipelines that converts model to anything that is needed. Existing model structure can
  35. * be used to generate events (listening to {@link engine.model.Document.change Document change event} is not required)
  36. * and custom callbacks can be added to the events (these does not have to be limited to changes in the view).
  37. *
  38. * When providing your own event listeners for `ModelConversionDispatcher` keep in mind that any callback that had
  39. * {@link engine.conversion.ModelConsumable#consume consumed} a value from consumable (and did some changes, i.e. to
  40. * the view) should also stop the event. This is because whenever a callback is fired it is assumed that there is something
  41. * to be consumed. Thanks to that approach, you do not have to test whether there is anything to consume at the beginning
  42. * of your listener callback.
  43. *
  44. * Example of providing a converter for `ModelConversionDispatcher`:
  45. *
  46. * // We will convert inserting "paragraph" model element into the model.
  47. * modelDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
  48. * // Remember to consume the part of consumable.
  49. * consumable.consume( data.item, 'insert' );
  50. *
  51. * // Translate position in model to position in the view.
  52. * const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  53. *
  54. * // Create a P element (note that this converter is for inserting P elements -> 'insert:paragraph').
  55. * const viewElement = new ViewElement( 'p' );
  56. *
  57. * // Bind the newly created view element to model element so positions will map accordingly in future.
  58. * conversionApi.mapper.bindElements( data.item, viewElement );
  59. *
  60. * // Add the newly created view element to the view.
  61. * viewWriter.insert( viewPosition, viewElement );
  62. *
  63. * // Remember to stop the event propagation if the data.item was consumed.
  64. * evt.stop();
  65. * } );
  66. *
  67. * Callback that "overrides" other callback:
  68. *
  69. * // Special converter for `linkHref` attribute added on custom `quote` element. Note, that this
  70. * // attribute may be the same as the attribute added by other features (link feature in this case).
  71. * // It might be even added by that feature! It makes sense that a part of content that is a quote is linked
  72. * // to an external source so it makes sense that link feature works on the custom quote element.
  73. * // However, we have to make sure that the attributes added by link feature are correctly converted.
  74. * // To block default `linkHref` conversion we have to:
  75. * // 1) add this callback with higher priority than link feature callback,
  76. * // 2) consume `linkHref` attribute add change.
  77. * modelConversionDispatcher.on( 'addAttribute:linkHref:quote', ( evt, data, consumable, conversionApi ) => {
  78. * consumable.consume( data.item, 'addAttribute:linkHref' );
  79. *
  80. * // Create a button that will represent the `linkHref` attribute.
  81. * let viewSourceBtn = new ViewElement( 'a', {
  82. * href: data.item.getAttribute( 'linkHref' ),
  83. * title: 'source'
  84. * } );
  85. *
  86. * // Add a class for the button.
  87. * viewSourceBtn.addClass( 'source' );
  88. *
  89. * // Insert the button using writer API.
  90. * // If `addAttribute` event is fired by `engine.conversion.ModelConversionDispatcher#convertInsert` it is fired
  91. * // after `data.item` insert conversion was done. If the event is fired due to attribute insertion coming from
  92. * // different source, `data.item` already existed. This means we are safe to get `viewQuote` from mapper.
  93. * const viewQuote = conversionApi.mapper.toViewElement( data.item );
  94. * const position = new ViewPosition( viewQuote, viewQuote.childCount );
  95. * viewWriter.insert( position, viewSourceBtn );
  96. *
  97. * evt.stop();
  98. * }, { priority: 'high' } );
  99. *
  100. * @memberOf engine.conversion
  101. */
  102. export default class ModelConversionDispatcher {
  103. /**
  104. * Creates a `ModelConversionDispatcher` that operates using passed API.
  105. *
  106. * @param {Object} [conversionApi] Interface passed by dispatcher to the events callbacks.
  107. */
  108. constructor( conversionApi = {} ) {
  109. /**
  110. * Interface passed by dispatcher to the events callbacks.
  111. *
  112. * @member {Object} engine.conversion.ModelConversionDispatcher#conversionApi
  113. */
  114. this.conversionApi = extend( { dispatcher: this }, conversionApi );
  115. }
  116. /**
  117. * Prepares data and fires a proper event.
  118. *
  119. * The method is crafted to take use of parameters passed in {@link engine.model.Document.change Document change event}.
  120. *
  121. * @see engine.model.Document.change
  122. * @fires engine.conversion.ModelConversionDispatcher#insert
  123. * @fires engine.conversion.ModelConversionDispatcher#move
  124. * @fires engine.conversion.ModelConversionDispatcher#remove
  125. * @fires engine.conversion.ModelConversionDispatcher#rename
  126. * @fires engine.conversion.ModelConversionDispatcher#addAttribute
  127. * @fires engine.conversion.ModelConversionDispatcher#removeAttribute
  128. * @fires engine.conversion.ModelConversionDispatcher#changeAttribute
  129. * @param {String} type Change type.
  130. * @param {Object} data Additional information about the change.
  131. */
  132. convertChange( type, data ) {
  133. // Do not convert changes if they happen in graveyard.
  134. // Graveyard is a special root that has no view / no other representation and changes done in it should not be converted.
  135. if ( type !== 'remove' && data.range && data.range.root.rootName == '$graveyard' ) {
  136. return;
  137. }
  138. if ( type == 'remove' && data.sourcePosition.root.rootName == '$graveyard' ) {
  139. return;
  140. }
  141. if ( type == 'rename' && data.element.root.rootName == '$graveyard' ) {
  142. return;
  143. }
  144. // We can safely dispatch changes.
  145. if ( type == 'insert' || type == 'reinsert' ) {
  146. this.convertInsertion( data.range );
  147. } else if ( type == 'move' ) {
  148. this.convertMove( data.sourcePosition, data.range );
  149. } else if ( type == 'remove' ) {
  150. this.convertRemove( data.sourcePosition, data.range );
  151. } else if ( type == 'addAttribute' || type == 'removeAttribute' || type == 'changeAttribute' ) {
  152. this.convertAttribute( type, data.range, data.key, data.oldValue, data.newValue );
  153. } else if ( type == 'rename' ) {
  154. this.convertRename( data.element, data.oldName );
  155. }
  156. }
  157. /**
  158. * Analyzes given range and fires insertion-connected events with data based on that range.
  159. *
  160. * **Note**: This method will fire separate events for node insertion and attributes insertion. All
  161. * attributes that are set on inserted nodes are treated like they were added just after node insertion.
  162. *
  163. * @fires engine.conversion.ModelConversionDispatcher#insert
  164. * @fires engine.conversion.ModelConversionDispatcher#addAttribute
  165. * @param {engine.model.Range} range Inserted range.
  166. */
  167. convertInsertion( range ) {
  168. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  169. const consumable = this._createInsertConsumable( range );
  170. // Fire a separate insert event for each node and text fragment contained in the range.
  171. for ( let value of range ) {
  172. const item = value.item;
  173. const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
  174. const data = {
  175. item: item,
  176. range: itemRange
  177. };
  178. this._testAndFire( 'insert', data, consumable );
  179. // Fire a separate addAttribute event for each attribute that was set on inserted items.
  180. // This is important because most attributes converters will listen only to add/change/removeAttribute events.
  181. // If we would not add this part, attributes on inserted nodes would not be converted.
  182. for ( let key of item.getAttributeKeys() ) {
  183. data.attributeKey = key;
  184. data.attributeOldValue = null;
  185. data.attributeNewValue = item.getAttribute( key );
  186. this._testAndFire( 'addAttribute:' + key, data, consumable );
  187. }
  188. }
  189. }
  190. /**
  191. * Fires move event with data based on passed values.
  192. *
  193. * @fires engine.conversion.ModelConversionDispatcher#move
  194. * @param {engine.model.Position} sourcePosition Position from where the range has been moved.
  195. * @param {engine.model.Range} range Moved range (after move).
  196. */
  197. convertMove( sourcePosition, range ) {
  198. // Keep in mind that move dispatcher expects flat range.
  199. const consumable = this._createConsumableForRange( range, 'move' );
  200. const items = Array.from( range.getItems( { shallow: true } ) );
  201. const rangeSize = range.end.offset - range.start.offset;
  202. const inSameParent = sourcePosition.parent == range.start.parent;
  203. let offset = 0;
  204. for ( let item of items ) {
  205. const data = {
  206. sourcePosition: sourcePosition,
  207. targetPosition: inSameParent ? range.start.getShiftedBy( rangeSize ) : range.start.getShiftedBy( offset ),
  208. item: item
  209. };
  210. offset += data.item.offsetSize;
  211. this._testAndFire( 'move', data, consumable );
  212. }
  213. }
  214. /**
  215. * Fires remove event with data based on passed values.
  216. *
  217. * @fires engine.conversion.ModelConversionDispatcher#remove
  218. * @param {engine.model.Position} sourcePosition Position from where the range has been removed.
  219. * @param {engine.model.Range} range Removed range (after remove, in {@link engine.model.Document#graveyard graveyard root}).
  220. */
  221. convertRemove( sourcePosition, range ) {
  222. const consumable = this._createConsumableForRange( range, 'remove' );
  223. for ( let item of range.getItems( { shallow: true } ) ) {
  224. const data = {
  225. sourcePosition: sourcePosition,
  226. item: item
  227. };
  228. this._testAndFire( 'remove', data, consumable );
  229. }
  230. }
  231. /**
  232. * Analyzes given attribute change and fires attributes-connected events with data based on passed values.
  233. *
  234. * @fires engine.conversion.ModelConversionDispatcher#addAttribute
  235. * @fires engine.conversion.ModelConversionDispatcher#removeAttribute
  236. * @fires engine.conversion.ModelConversionDispatcher#changeAttribute
  237. * @param {String} type Change type. Possible values: `addAttribute`, `removeAttribute`, `changeAttribute`.
  238. * @param {engine.model.Range} range Changed range.
  239. * @param {String} key Attribute key.
  240. * @param {*} oldValue Attribute value before the change or `null` if attribute has not been set.
  241. * @param {*} newValue New attribute value or `null` if attribute has been removed.
  242. */
  243. convertAttribute( type, range, key, oldValue, newValue ) {
  244. // Create a list with attributes to consume.
  245. const consumable = this._createConsumableForRange( range, type + ':' + key );
  246. // Create a separate attribute event for each node in the range.
  247. for ( let value of range ) {
  248. const item = value.item;
  249. const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
  250. const data = {
  251. item: item,
  252. range: itemRange,
  253. attributeKey: key,
  254. attributeOldValue: oldValue,
  255. attributeNewValue: newValue
  256. };
  257. this._testAndFire( type + ':' + key, data, consumable, this.conversionApi );
  258. }
  259. }
  260. /**
  261. * Fires rename event with data based on passed values.
  262. *
  263. * @fires engine.conversion.ModelConversionDispatcher#event:rename
  264. * @param {engine.view.Element} element Renamed element.
  265. * @param {String} oldName Name of the renamed element before it was renamed.
  266. */
  267. convertRename( element, oldName ) {
  268. const consumable = new Consumable();
  269. consumable.add( element, 'rename' );
  270. const data = { element, oldName };
  271. this.fire( 'rename:' + element.name + ':' + oldName, data, consumable, this.conversionApi );
  272. }
  273. /**
  274. * Fires events for given {@link engine.model.Selection selection} to start selection conversion.
  275. *
  276. * @fires engine.conversion.ModelConversionDispatcher#selection
  277. * @fires engine.conversion.ModelConversionDispatcher#selectionAttribute
  278. * @param {engine.model.Selection} selection Selection to convert.
  279. */
  280. convertSelection( selection ) {
  281. const consumable = this._createSelectionConsumable( selection );
  282. const data = {
  283. selection: selection
  284. };
  285. this.fire( 'selection', data, consumable, this.conversionApi );
  286. for ( let key of selection.getAttributeKeys() ) {
  287. data.key = key;
  288. data.value = selection.getAttribute( key );
  289. // Do not fire event if the attribute has been consumed.
  290. if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
  291. this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
  292. }
  293. }
  294. }
  295. /**
  296. * Creates {@link engine.conversion.ModelConsumable} with values to consume from given range, assuming that
  297. * given range has just been inserted to the model.
  298. *
  299. * @private
  300. * @param {engine.model.Range} range Inserted range.
  301. * @returns {engine.conversion.ModelConsumable} Values to consume.
  302. */
  303. _createInsertConsumable( range ) {
  304. const consumable = new Consumable();
  305. for ( let value of range ) {
  306. const item = value.item;
  307. consumable.add( item, 'insert' );
  308. for ( let key of item.getAttributeKeys() ) {
  309. consumable.add( item, 'addAttribute:' + key );
  310. }
  311. }
  312. return consumable;
  313. }
  314. /**
  315. * Creates {@link engine.conversion.ModelConsumable} with values of given `type` for each item from given `range`.
  316. *
  317. * @private
  318. * @param {engine.conversion.Range} range Affected range.
  319. * @param {String} type Consumable type.
  320. * @returns {engine.conversion.ModelConsumable} Values to consume.
  321. */
  322. _createConsumableForRange( range, type ) {
  323. const consumable = new Consumable();
  324. for ( let item of range.getItems() ) {
  325. consumable.add( item, type );
  326. }
  327. return consumable;
  328. }
  329. /**
  330. * Creates {@link engine.conversion.ModelConsumable} with selection consumable values.
  331. *
  332. * @private
  333. * @param {engine.model.Selection} selection Selection to create consumable from.
  334. * @returns {engine.conversion.ModelConsumable} Values to consume.
  335. */
  336. _createSelectionConsumable( selection ) {
  337. const consumable = new Consumable();
  338. consumable.add( selection, 'selection' );
  339. for ( let key of selection.getAttributeKeys() ) {
  340. consumable.add( selection, 'selectionAttribute:' + key );
  341. }
  342. return consumable;
  343. }
  344. /**
  345. * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
  346. *
  347. * @private
  348. * @fires engine.conversion.ModelConversionDispatcher#insert
  349. * @fires engine.conversion.ModelConversionDispatcher#addAttribute
  350. * @fires engine.conversion.ModelConversionDispatcher#removeAttribute
  351. * @fires engine.conversion.ModelConversionDispatcher#changeAttribute
  352. * @param {String} type Event type.
  353. * @param {Object} data Event data.
  354. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  355. */
  356. _testAndFire( type, data, consumable ) {
  357. if ( !consumable.test( data.item, type ) ) {
  358. // Do not fire event if the item was consumed.
  359. return;
  360. }
  361. if ( type === 'insert' || type === 'remove' || type == 'move' ) {
  362. if ( data.item instanceof TextProxy ) {
  363. // Example: insert:$text.
  364. this.fire( type + ':$text', data, consumable, this.conversionApi );
  365. } else {
  366. // Example: insert:paragraph.
  367. this.fire( type + ':' + data.item.name, data, consumable, this.conversionApi );
  368. }
  369. } else {
  370. // Example addAttribute:alt:img.
  371. // Example addAttribute:bold:$text.
  372. const name = data.item.name || '$text';
  373. this.fire( type + ':' + name, data, consumable, this.conversionApi );
  374. }
  375. }
  376. /**
  377. * Fired for inserted nodes.
  378. *
  379. * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
  380. * `insert:<type>:<elementName>`. `type` is either `text` when one or more characters has been inserted or `element`
  381. * when {@link engine.model.Element} has been inserted. If `type` is `element`, `elementName` is added and is
  382. * equal to the {@link engine.model.Element#name name} of inserted element. This way listeners can either
  383. * listen to very general `insert` event or, i.e., very specific `insert:paragraph` event, which is fired only for
  384. * model elements with name `paragraph`.
  385. *
  386. * @event engine.conversion.ModelConversionDispatcher.insert
  387. * @param {Object} data Additional information about the change.
  388. * @param {engine.model.Item} data.item Inserted item.
  389. * @param {engine.model.Range} data.range Range spanning over inserted item.
  390. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  391. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  392. */
  393. /**
  394. * Fired for moved nodes.
  395. *
  396. * @event engine.conversion.ModelConversionDispatcher.move
  397. * @param {Object} data Additional information about the change.
  398. * @param {engine.model.Position} data.sourcePosition Position from where the range has been moved.
  399. * @param {engine.model.Range} data.range Moved range (after move).
  400. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  401. */
  402. /**
  403. * Fired for removed nodes.
  404. *
  405. * @event engine.conversion.ModelConversionDispatcher.remove
  406. * @param {Object} data Additional information about the change.
  407. * @param {engine.model.Position} data.sourcePosition Position from where the range has been removed.
  408. * @param {engine.model.Range} data.range Removed range (in {@link engine.model.Document#graveyard graveyard root}).
  409. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  410. */
  411. /**
  412. * Fired for renamed element.
  413. *
  414. * @event engine.conversion.ModelConversionDispatcher.rename
  415. * @param {Object} data Additional information about the change.
  416. * @param {engine.model.Element} data.element Renamed element.
  417. * @param {String} data.oldName Old name of the renamed element.
  418. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  419. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  420. */
  421. /**
  422. * Fired when attribute has been added on a node.
  423. *
  424. * `addAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  425. * `addAttribute:<attributeKey>:<elementName>`. `attributeKey` is the key of added attribute. `elementName` is
  426. * equal to the {@link engine.model.Element#name name} of the element which got the attribute. This way listeners
  427. * can either listen to adding certain attribute, i.e. `addAttribute:bold`, or be more specific, i.e. `addAttribute:link:img`.
  428. *
  429. * @event engine.conversion.ModelConversionDispatcher.addAttribute
  430. * @param {Object} data Additional information about the change.
  431. * @param {engine.model.Item} data.item Changed item.
  432. * @param {engine.model.Range} data.range Range spanning over changed item.
  433. * @param {String} data.attributeKey Attribute key.
  434. * @param {null} data.attributeOldValue Attribute value before the change - always `null`. Kept for the sake of unifying events.
  435. * @param {*} data.attributeNewValue New attribute value.
  436. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  437. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  438. */
  439. /**
  440. * Fired when attribute has been removed from a node.
  441. *
  442. * `removeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  443. * `removeAttribute:<attributeKey>:<elementName>`. `attributeKey` is the key of removed attribute. `elementName` is
  444. * equal to the {@link engine.model.Element#name name} of the element which got the attribute removed. This way listeners
  445. * can either listen to removing certain attribute, i.e. `removeAttribute:bold`, or be more specific, i.e. `removeAttribute:link:img`.
  446. *
  447. * @event engine.conversion.ModelConversionDispatcher.removeAttribute
  448. * @param {Object} data Additional information about the change.
  449. * @param {engine.model.Item} data.item Changed item.
  450. * @param {engine.model.Range} data.range Range spanning over changed item.
  451. * @param {String} data.attributeKey Attribute key.
  452. * @param {*} data.attributeOldValue Attribute value before it was removed.
  453. * @param {null} data.attributeNewValue New attribute value - always `null`. Kept for the sake of unifying events.
  454. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  455. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  456. */
  457. /**
  458. * Fired when attribute of a node has been changed.
  459. *
  460. * `changeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  461. * `changeAttribute:<attributeKey>:<elementName>`. `attributeKey` is the key of changed attribute. `elementName` is
  462. * equal to the {@link engine.model.Element#name name} of the element which got the attribute changed. This way listeners
  463. * can either listen to changing certain attribute, i.e. `changeAttribute:link`, or be more specific, i.e. `changeAttribute:link:img`.
  464. *
  465. * @event engine.conversion.ModelConversionDispatcher.changeAttribute
  466. * @param {Object} data Additional information about the change.
  467. * @param {engine.model.Item} data.item Changed item.
  468. * @param {engine.model.Range} data.range Range spanning over changed item.
  469. * @param {String} data.attributeKey Attribute key.
  470. * @param {*} data.attributeOldValue Attribute value before the change.
  471. * @param {*} data.attributeNewValue New attribute value.
  472. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  473. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  474. */
  475. /**
  476. * Fired for {@link engine.model.Selection selection} changes.
  477. *
  478. * @event engine.conversion.ModelConversionDispatcher.selection
  479. * @param {engine.model.Selection} selection `Selection` instance that is converted.
  480. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  481. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  482. */
  483. /**
  484. * Fired for {@link engine.model.Selection selection} attributes changes.
  485. *
  486. * `selectionAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  487. * `selectionAttribute:<attributeKey>`. `attributeKey` is the key of selection attribute. This way listen can listen to
  488. * certain attribute, i.e. `addAttribute:bold`.
  489. *
  490. * @event engine.conversion.ModelConversionDispatcher.selectionAttribute
  491. * @param {Object} data Additional information about the change.
  492. * @param {engine.model.Selection} data.selection Selection that is converted.
  493. * @param {String} data.attributeKey Key of changed attribute.
  494. * @param {*} data.attributeValue Value of changed attribute.
  495. * @param {engine.conversion.ModelConsumable} consumable Values to consume.
  496. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  497. */
  498. }
  499. mix( ModelConversionDispatcher, EmitterMixin );