downcastdispatcher.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/conversion/downcastdispatcher
  7. */
  8. import Consumable from './modelconsumable';
  9. import Range from '../model/range';
  10. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  11. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  12. import { extend } from 'lodash-es';
  13. /**
  14. * `DowncastDispatcher` is a central point of downcasting (conversion from model to view), which is a process of reacting to changes
  15. * in the model and firing a set of events. Callbacks listening to those events are called converters. Those
  16. * converters role is to convert the model changes to changes in view (for example, adding view nodes or
  17. * changing attributes on view elements).
  18. *
  19. * During conversion process, `DowncastDispatcher` fires events, basing on state of the model and prepares
  20. * data for those events. It is important to understand that those events are connected with changes done on model,
  21. * for example: "node has been inserted" or "attribute has changed". This is in a contrary to upcasting (view to model conversion),
  22. * where we convert view state (view nodes) to a model tree.
  23. *
  24. * The events are prepared basing on a diff created by {@link module:engine/model/differ~Differ Differ}, which buffers them
  25. * and then passes to `DowncastDispatcher` as a diff between old model state and new model state.
  26. *
  27. * Note, that because changes are converted there is a need to have a mapping between model structure and view structure.
  28. * To map positions and elements during downcast (model to view conversion) use {@link module:engine/conversion/mapper~Mapper}.
  29. *
  30. * `DowncastDispatcher` fires following events for model tree changes:
  31. *
  32. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
  33. * if a range of nodes has been inserted to the model tree,
  34. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:remove remove}
  35. * if a range of nodes has been removed from the model tree,
  36. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute}
  37. * if attribute has been added, changed or removed from a model node.
  38. *
  39. * For {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert}
  40. * and {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute},
  41. * `DowncastDispatcher` generates {@link module:engine/conversion/modelconsumable~ModelConsumable consumables}.
  42. * These are used to have a control over which changes has been already consumed. It is useful when some converters
  43. * overwrite other or converts multiple changes (for example converts insertion of an element and also converts that
  44. * element's attributes during insertion).
  45. *
  46. * Additionally, `DowncastDispatcher` fires events for {@link module:engine/model/markercollection~Marker marker} changes:
  47. *
  48. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} if a marker has been added,
  49. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} if a marker has been removed.
  50. *
  51. * Note, that changing a marker is done through removing the marker from the old range, and adding on the new range,
  52. * so both those events are fired.
  53. *
  54. * Finally, `DowncastDispatcher` also handles firing events for {@link module:engine/model/selection model selection}
  55. * conversion:
  56. *
  57. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:selection}
  58. * which converts selection from model to view,
  59. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute}
  60. * which is fired for every selection attribute,
  61. * * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}
  62. * which is fired for every marker which contains selection.
  63. *
  64. * Unlike model tree and markers, events for selection are not fired for changes but for selection state.
  65. *
  66. * When providing custom listeners for `DowncastDispatcher` remember to check whether given change has not been
  67. * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} yet.
  68. *
  69. * When providing custom listeners for `DowncastDispatcher` keep in mind that any callback that had
  70. * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
  71. * converted the change should also stop the event (for efficiency purposes).
  72. *
  73. * When providing custom listeners for `DowncastDispatcher` remember to use provided
  74. * {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} to apply changes to the view document.
  75. *
  76. * Example of a custom converter for `DowncastDispatcher`:
  77. *
  78. * // We will convert inserting "paragraph" model element into the model.
  79. * downcastDispatcher.on( 'insert:paragraph', ( evt, data, conversionApi ) => {
  80. * // Remember to check whether the change has not been consumed yet and consume it.
  81. * if ( conversionApi.consumable.consume( data.item, 'insert' ) ) {
  82. * return;
  83. * }
  84. *
  85. * // Translate position in model to position in view.
  86. * const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  87. *
  88. * // Create <p> element that will be inserted in view at `viewPosition`.
  89. * const viewElement = conversionApi.writer.createContainerElement( 'p' );
  90. *
  91. * // Bind the newly created view element to model element so positions will map accordingly in future.
  92. * conversionApi.mapper.bindElements( data.item, viewElement );
  93. *
  94. * // Add the newly created view element to the view.
  95. * conversionApi.writer.insert( viewPosition, viewElement );
  96. *
  97. * // Remember to stop the event propagation.
  98. * evt.stop();
  99. * } );
  100. */
  101. export default class DowncastDispatcher {
  102. /**
  103. * Creates a `DowncastDispatcher` instance.
  104. *
  105. * @see module:engine/conversion/downcastdispatcher~DowncastConversionApi
  106. * @param {Object} conversionApi Additional properties for interface that will be passed to events fired
  107. * by `DowncastDispatcher`.
  108. */
  109. constructor( conversionApi ) {
  110. /**
  111. * Interface passed by dispatcher to the events callbacks.
  112. *
  113. * @member {module:engine/conversion/downcastdispatcher~DowncastConversionApi}
  114. */
  115. this.conversionApi = extend( { dispatcher: this }, conversionApi );
  116. }
  117. /**
  118. * Takes {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
  119. *
  120. * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
  121. * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with converted model.
  122. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  123. */
  124. convertChanges( differ, markers, writer ) {
  125. // Before the view is updated, remove markers which have changed.
  126. for ( const change of differ.getMarkersToRemove() ) {
  127. this.convertMarkerRemove( change.name, change.range, writer );
  128. }
  129. // Convert changes that happened on model tree.
  130. for ( const entry of differ.getChanges() ) {
  131. if ( entry.type == 'insert' ) {
  132. this.convertInsert( Range._createFromPositionAndShift( entry.position, entry.length ), writer );
  133. } else if ( entry.type == 'remove' ) {
  134. this.convertRemove( entry.position, entry.length, entry.name, writer );
  135. } else {
  136. // entry.type == 'attribute'.
  137. this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer );
  138. }
  139. }
  140. for ( const markerName of this.conversionApi.mapper.flushUnboundMarkerNames() ) {
  141. const markerRange = markers.get( markerName ).getRange();
  142. this.convertMarkerRemove( markerName, markerRange, writer );
  143. this.convertMarkerAdd( markerName, markerRange, writer );
  144. }
  145. // After the view is updated, convert markers which have changed.
  146. for ( const change of differ.getMarkersToAdd() ) {
  147. this.convertMarkerAdd( change.name, change.range, writer );
  148. }
  149. }
  150. /**
  151. * Starts conversion of a range insertion.
  152. *
  153. * For each node in the range, {@link #event:insert insert event is fired}. For each attribute on each node,
  154. * {@link #event:attribute attribute event is fired}.
  155. *
  156. * @fires insert
  157. * @fires attribute
  158. * @param {module:engine/model/range~Range} range Inserted range.
  159. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  160. */
  161. convertInsert( range, writer ) {
  162. this.conversionApi.writer = writer;
  163. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  164. this.conversionApi.consumable = this._createInsertConsumable( range );
  165. // Fire a separate insert event for each node and text fragment contained in the range.
  166. for ( const value of range ) {
  167. const item = value.item;
  168. const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
  169. const data = {
  170. item,
  171. range: itemRange
  172. };
  173. this._testAndFire( 'insert', data );
  174. // Fire a separate addAttribute event for each attribute that was set on inserted items.
  175. // This is important because most attributes converters will listen only to add/change/removeAttribute events.
  176. // If we would not add this part, attributes on inserted nodes would not be converted.
  177. for ( const key of item.getAttributeKeys() ) {
  178. data.attributeKey = key;
  179. data.attributeOldValue = null;
  180. data.attributeNewValue = item.getAttribute( key );
  181. this._testAndFire( `attribute:${ key }`, data );
  182. }
  183. }
  184. this._clearConversionApi();
  185. }
  186. /**
  187. * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
  188. *
  189. * @param {module:engine/model/position~Position} position Position from which node was removed.
  190. * @param {Number} length Offset size of removed node.
  191. * @param {String} name Name of removed node.
  192. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  193. */
  194. convertRemove( position, length, name, writer ) {
  195. this.conversionApi.writer = writer;
  196. this.fire( 'remove:' + name, { position, length }, this.conversionApi );
  197. this._clearConversionApi();
  198. }
  199. /**
  200. * Starts conversion of attribute change on given `range`.
  201. *
  202. * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
  203. *
  204. * @fires attribute
  205. * @param {module:engine/model/range~Range} range Changed range.
  206. * @param {String} key Key of the attribute that has changed.
  207. * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
  208. * @param {*} newValue New attribute value or `null` if the attribute has been removed.
  209. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  210. */
  211. convertAttribute( range, key, oldValue, newValue, writer ) {
  212. this.conversionApi.writer = writer;
  213. // Create a list with attributes to consume.
  214. this.conversionApi.consumable = this._createConsumableForRange( range, `attribute:${ key }` );
  215. // Create a separate attribute event for each node in the range.
  216. for ( const value of range ) {
  217. const item = value.item;
  218. const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
  219. const data = {
  220. item,
  221. range: itemRange,
  222. attributeKey: key,
  223. attributeOldValue: oldValue,
  224. attributeNewValue: newValue
  225. };
  226. this._testAndFire( `attribute:${ key }`, data );
  227. }
  228. this._clearConversionApi();
  229. }
  230. /**
  231. * Starts model selection conversion.
  232. *
  233. * Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
  234. *
  235. * @fires selection
  236. * @fires addMarker
  237. * @fires attribute
  238. * @param {module:engine/model/selection~Selection} selection Selection to convert.
  239. * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with converted model.
  240. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  241. */
  242. convertSelection( selection, markers, writer ) {
  243. const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
  244. this.conversionApi.writer = writer;
  245. this.conversionApi.consumable = this._createSelectionConsumable( selection, markersAtSelection );
  246. this.fire( 'selection', { selection }, this.conversionApi );
  247. if ( !selection.isCollapsed ) {
  248. return;
  249. }
  250. for ( const marker of markersAtSelection ) {
  251. const markerRange = marker.getRange();
  252. if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
  253. continue;
  254. }
  255. const data = {
  256. item: selection,
  257. markerName: marker.name,
  258. markerRange
  259. };
  260. if ( this.conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
  261. this.fire( 'addMarker:' + marker.name, data, this.conversionApi );
  262. }
  263. }
  264. for ( const key of selection.getAttributeKeys() ) {
  265. const data = {
  266. item: selection,
  267. range: selection.getFirstRange(),
  268. attributeKey: key,
  269. attributeOldValue: null,
  270. attributeNewValue: selection.getAttribute( key )
  271. };
  272. // Do not fire event if the attribute has been consumed.
  273. if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
  274. this.fire( 'attribute:' + data.attributeKey + ':$text', data, this.conversionApi );
  275. }
  276. }
  277. this._clearConversionApi();
  278. }
  279. /**
  280. * Converts added marker. Fires {@link #event:addMarker addMarker} event for each item
  281. * in marker's range. If range is collapsed single event is dispatched. See event description for more details.
  282. *
  283. * @fires addMarker
  284. * @param {String} markerName Marker name.
  285. * @param {module:engine/model/range~Range} markerRange Marker range.
  286. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  287. */
  288. convertMarkerAdd( markerName, markerRange, writer ) {
  289. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  290. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  291. return;
  292. }
  293. this.conversionApi.writer = writer;
  294. // In markers' case, event name == consumable name.
  295. const eventName = 'addMarker:' + markerName;
  296. //
  297. // First, fire an event for the whole marker.
  298. //
  299. const consumable = new Consumable();
  300. consumable.add( markerRange, eventName );
  301. this.conversionApi.consumable = consumable;
  302. this.fire( eventName, { markerName, markerRange }, this.conversionApi );
  303. //
  304. // Do not fire events for each item inside the range if the range got consumed.
  305. //
  306. if ( !consumable.test( markerRange, eventName ) ) {
  307. return;
  308. }
  309. //
  310. // Then, fire an event for each item inside the marker range.
  311. //
  312. this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
  313. for ( const item of markerRange.getItems() ) {
  314. // Do not fire event for already consumed items.
  315. if ( !this.conversionApi.consumable.test( item, eventName ) ) {
  316. continue;
  317. }
  318. const data = { item, range: Range._createOn( item ), markerName, markerRange };
  319. this.fire( eventName, data, this.conversionApi );
  320. }
  321. this._clearConversionApi();
  322. }
  323. /**
  324. * Fires conversion of marker removal. Fires {@link #event:removeMarker removeMarker} event with provided data.
  325. *
  326. * @fires removeMarker
  327. * @param {String} markerName Marker name.
  328. * @param {module:engine/model/range~Range} markerRange Marker range.
  329. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  330. */
  331. convertMarkerRemove( markerName, markerRange, writer ) {
  332. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  333. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  334. return;
  335. }
  336. this.conversionApi.writer = writer;
  337. this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
  338. this._clearConversionApi();
  339. }
  340. /**
  341. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range,
  342. * assuming that the range has just been inserted to the model.
  343. *
  344. * @private
  345. * @param {module:engine/model/range~Range} range Inserted range.
  346. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  347. */
  348. _createInsertConsumable( range ) {
  349. const consumable = new Consumable();
  350. for ( const value of range ) {
  351. const item = value.item;
  352. consumable.add( item, 'insert' );
  353. for ( const key of item.getAttributeKeys() ) {
  354. consumable.add( item, 'attribute:' + key );
  355. }
  356. }
  357. return consumable;
  358. }
  359. /**
  360. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for given range.
  361. *
  362. * @private
  363. * @param {module:engine/model/range~Range} range Affected range.
  364. * @param {String} type Consumable type.
  365. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  366. */
  367. _createConsumableForRange( range, type ) {
  368. const consumable = new Consumable();
  369. for ( const item of range.getItems() ) {
  370. consumable.add( item, type );
  371. }
  372. return consumable;
  373. }
  374. /**
  375. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
  376. *
  377. * @private
  378. * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
  379. * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
  380. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  381. */
  382. _createSelectionConsumable( selection, markers ) {
  383. const consumable = new Consumable();
  384. consumable.add( selection, 'selection' );
  385. for ( const marker of markers ) {
  386. consumable.add( selection, 'addMarker:' + marker.name );
  387. }
  388. for ( const key of selection.getAttributeKeys() ) {
  389. consumable.add( selection, 'attribute:' + key );
  390. }
  391. return consumable;
  392. }
  393. /**
  394. * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
  395. *
  396. * @private
  397. * @fires insert
  398. * @fires attribute
  399. * @param {String} type Event type.
  400. * @param {Object} data Event data.
  401. */
  402. _testAndFire( type, data ) {
  403. if ( !this.conversionApi.consumable.test( data.item, type ) ) {
  404. // Do not fire event if the item was consumed.
  405. return;
  406. }
  407. const name = data.item.name || '$text';
  408. this.fire( type + ':' + name, data, this.conversionApi );
  409. }
  410. /**
  411. * Clears conversion API object.
  412. *
  413. * @private
  414. */
  415. _clearConversionApi() {
  416. delete this.conversionApi.writer;
  417. delete this.conversionApi.consumable;
  418. }
  419. /**
  420. * Fired for inserted nodes.
  421. *
  422. * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
  423. * `insert:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been inserted,
  424. * or {@link module:engine/model/element~Element#name name} of inserted element.
  425. *
  426. * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
  427. *
  428. * @event insert
  429. * @param {Object} data Additional information about the change.
  430. * @param {module:engine/model/item~Item} data.item Inserted item.
  431. * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
  432. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  433. * to be used by callback, passed in `DowncastDispatcher` constructor.
  434. */
  435. /**
  436. * Fired for removed nodes.
  437. *
  438. * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
  439. * `remove:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been removed,
  440. * or the {@link module:engine/model/element~Element#name name} of removed element.
  441. *
  442. * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
  443. *
  444. * @event remove
  445. * @param {Object} data Additional information about the change.
  446. * @param {module:engine/model/position~Position} data.position Position from which the node has been removed.
  447. * @param {Number} data.length Offset size of the removed node.
  448. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  449. * to be used by callback, passed in `DowncastDispatcher` constructor.
  450. */
  451. /**
  452. * Fired in the following cases:
  453. *
  454. * * when an attribute has been added, changed, or removed from a node,
  455. * * when a node with an attribute is inserted,
  456. * * when collapsed model selection attribute is converted.
  457. *
  458. * `attribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  459. * `attribute:attributeKey:name`. `attributeKey` is the key of added/changed/removed attribute.
  460. * `name` is either `'$text'` if change was on {@link module:engine/model/text~Text a text node},
  461. * or the {@link module:engine/model/element~Element#name name} of element which attribute has changed.
  462. *
  463. * This way listeners can either listen to a general `attribute:bold` event or specific event (for example `attribute:src:image`).
  464. *
  465. * @event attribute
  466. * @param {Object} data Additional information about the change.
  467. * @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} data.item Changed item
  468. * or converted selection.
  469. * @param {module:engine/model/range~Range} data.range Range spanning over changed item or selection range.
  470. * @param {String} data.attributeKey Attribute key.
  471. * @param {*} data.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
  472. * @param {*} data.attributeNewValue New attribute value.
  473. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  474. * to be used by callback, passed in `DowncastDispatcher` constructor.
  475. */
  476. /**
  477. * Fired for {@link module:engine/model/selection~Selection selection} changes.
  478. *
  479. * @event selection
  480. * @param {module:engine/model/selection~Selection} selection Selection that is converted.
  481. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  482. * to be used by callback, passed in `DowncastDispatcher` constructor.
  483. */
  484. /**
  485. * Fired when a new marker is added to the model. Also fired when collapsed model selection that is inside marker is converted.
  486. *
  487. * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  488. * `addMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  489. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
  490. * `addMarker:foo:bar` events.
  491. *
  492. * If the marker range is not collapsed:
  493. *
  494. * * the event is fired for each item in the marker range one by one,
  495. * * `conversionApi.consumable` includes each item of the marker range and the consumable value is same as event name.
  496. *
  497. * If the marker range is collapsed:
  498. *
  499. * * there is only one event,
  500. * * `conversionApi.consumable` includes marker range with event name.
  501. *
  502. * If selection inside a marker is converted:
  503. *
  504. * * there is only one event,
  505. * * `conversionApi.consumable` includes selection instance with event name.
  506. *
  507. * @event addMarker
  508. * @param {Object} data Additional information about the change.
  509. * @param {module:engine/model/item~Item|module:engine/model/selection~Selection} data.item Item inside the new marker or
  510. * the selection that is being converted.
  511. * @param {module:engine/model/range~Range} [data.range] Range spanning over converted item. Available only in marker conversion, if
  512. * the marker range was not collapsed.
  513. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  514. * @param {String} data.markerName Marker name.
  515. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  516. * to be used by callback, passed in `DowncastDispatcher` constructor.
  517. */
  518. /**
  519. * Fired when marker is removed from the model.
  520. *
  521. * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  522. * `removeMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  523. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
  524. * `removeMarker:foo:bar` events.
  525. *
  526. * @event removeMarker
  527. * @param {Object} data Additional information about the change.
  528. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  529. * @param {String} data.markerName Marker name.
  530. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  531. * to be used by callback, passed in `DowncastDispatcher` constructor.
  532. */
  533. }
  534. mix( DowncastDispatcher, EmitterMixin );
  535. // Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
  536. // converted if they happen inside an element with custom conversion method.
  537. //
  538. // @param {module:engine/model/position~Position} modelPosition
  539. // @param {module:engine/model/markercollection~Marker} marker
  540. // @param {module:engine/conversion/mapper~Mapper} mapper
  541. // @returns {Boolean}
  542. function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
  543. const range = marker.getRange();
  544. const ancestors = Array.from( modelPosition.getAncestors() );
  545. ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
  546. ancestors.reverse();
  547. const hasCustomHandling = ancestors.some( element => {
  548. if ( range.containsItem( element ) ) {
  549. const viewElement = mapper.toViewElement( element );
  550. return !!viewElement.getCustomProperty( 'addHighlight' );
  551. }
  552. } );
  553. return !hasCustomHandling;
  554. }
  555. /**
  556. * Conversion interface that is registered for given {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
  557. * and is passed as one of parameters when {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher dispatcher}
  558. * fires it's events.
  559. *
  560. * @interface module:engine/conversion/downcastdispatcher~DowncastConversionApi
  561. */
  562. /**
  563. * The {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} instance.
  564. *
  565. * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #dispatcher
  566. */
  567. /**
  568. * Stores information about what parts of processed model item are still waiting to be handled. After a piece of model item
  569. * was converted, appropriate consumable value should be {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
  570. *
  571. * @member {module:engine/conversion/modelconsumable~ModelConsumable} #consumable
  572. */
  573. /**
  574. * The {@link module:engine/conversion/mapper~Mapper} instance.
  575. *
  576. * @member {module:engine/conversion/mapper~Mapper} #mapper
  577. */
  578. /**
  579. * The {@link module:engine/view/downcastwriter~DowncastWriter} instance used to manipulate data during conversion.
  580. *
  581. * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
  582. */