modelconversiondispatcher.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/conversion/modelconversiondispatcher
  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 '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  13. /**
  14. * `ModelConversionDispatcher` is a central point of model conversion, 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, `ModelConversionDispatcher` 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 contrary to 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 `ModelConversionDispatcher` 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 model to view conversion use {@link module:engine/conversion/mapper~Mapper}.
  29. *
  30. * `ModelConversionDispatcher` fires following events for model tree changes:
  31. *
  32. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert}
  33. * if a range of nodes has been inserted to the model tree,
  34. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:remove remove}
  35. * if a range of nodes has been removed from the model tree,
  36. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute}
  37. * if attribute has been added, changed or removed from a model node.
  38. *
  39. * For {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert}
  40. * and {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute},
  41. * `ModelConversionDispatcher` 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, `ModelConversionDispatcher` fires events for {@link module:engine/model/markercollection~Marker marker} changes:
  47. *
  48. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker} if a marker has been added,
  49. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#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, `ModelConversionDispatcher` also handles firing events for {@link module:engine/model/selection model selection}
  55. * conversion:
  56. *
  57. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:selection}
  58. * which converts selection from model to view,
  59. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:selectionAttribute}
  60. * which is fired for every selection attribute,
  61. * * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:selectionMarker}
  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 `ModelConversionDispatcher` 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 `ModelConversionDispatcher` 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. * Example of a custom converter for `ModelConversionDispatcher`:
  74. *
  75. * // We will convert inserting "paragraph" model element into the model.
  76. * modelDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
  77. * // Remember to check whether the change has not been consumed yet and consume it.
  78. * if ( consumable.consume( data.item, 'insert' ) ) {
  79. * return;
  80. * }
  81. *
  82. * // Translate position in model to position in view.
  83. * const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  84. *
  85. * // Create <p> element that will be inserted in view at `viewPosition`.
  86. * const viewElement = new ViewElement( 'p' );
  87. *
  88. * // Bind the newly created view element to model element so positions will map accordingly in future.
  89. * conversionApi.mapper.bindElements( data.item, viewElement );
  90. *
  91. * // Add the newly created view element to the view.
  92. * viewWriter.insert( viewPosition, viewElement );
  93. *
  94. * // Remember to stop the event propagation.
  95. * evt.stop();
  96. * } );
  97. */
  98. export default class ModelConversionDispatcher {
  99. /**
  100. * Creates a `ModelConversionDispatcher` instance.
  101. *
  102. * @param {module:engine/model/model~Model} model Data model.
  103. * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls.
  104. */
  105. constructor( model, conversionApi = {} ) {
  106. /**
  107. * Data model instance bound with this dispatcher.
  108. *
  109. * @private
  110. * @member {module:engine/model/model~Model}
  111. */
  112. this._model = model;
  113. /**
  114. * Interface passed by dispatcher to the events callbacks.
  115. *
  116. * @member {Object}
  117. */
  118. this.conversionApi = extend( { dispatcher: this }, conversionApi );
  119. }
  120. /**
  121. * Takes {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
  122. *
  123. * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
  124. */
  125. convertChanges( differ ) {
  126. // Convert changes that happened on model tree.
  127. for ( const entry of differ.getChanges() ) {
  128. if ( entry.type == 'insert' ) {
  129. this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ) );
  130. } else if ( entry.type == 'remove' ) {
  131. this.convertRemove( entry.position, entry.length, entry.name );
  132. } else {
  133. // entry.type == 'attribute'.
  134. this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue );
  135. }
  136. }
  137. // After the view is updated, convert markers which has changed.
  138. for ( const change of differ.getMarkersToAdd() ) {
  139. this.convertMarkerAdd( change.name, change.range );
  140. }
  141. }
  142. /**
  143. * Starts conversion of a range insertion.
  144. *
  145. * For each node in the range, {@link #event:insert insert event is fired}. For each attribute on each node,
  146. * {@link #event:attribute attribute event is fired}.
  147. *
  148. * @fires insert
  149. * @fires attribute
  150. * @param {module:engine/model/range~Range} range Inserted range.
  151. */
  152. convertInsert( range ) {
  153. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  154. const consumable = this._createInsertConsumable( range );
  155. // Fire a separate insert event for each node and text fragment contained in the range.
  156. for ( const value of range ) {
  157. const item = value.item;
  158. const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
  159. const data = {
  160. item,
  161. range: itemRange
  162. };
  163. this._testAndFire( 'insert', data, consumable );
  164. // Fire a separate addAttribute event for each attribute that was set on inserted items.
  165. // This is important because most attributes converters will listen only to add/change/removeAttribute events.
  166. // If we would not add this part, attributes on inserted nodes would not be converted.
  167. for ( const key of item.getAttributeKeys() ) {
  168. data.attributeKey = key;
  169. data.attributeOldValue = null;
  170. data.attributeNewValue = item.getAttribute( key );
  171. this._testAndFire( `attribute:${ key }`, data, consumable );
  172. }
  173. }
  174. }
  175. /**
  176. * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
  177. *
  178. * @param {module:engine/model/position~Position} position Position from which node was removed.
  179. * @param {Number} length Offset size of removed node.
  180. * @param {String} name Name of removed node.
  181. */
  182. convertRemove( position, length, name ) {
  183. this.fire( 'remove:' + name, { position, length }, this.conversionApi );
  184. }
  185. /**
  186. * Starts conversion of attribute change on given `range`.
  187. *
  188. * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
  189. *
  190. * @fires attribute
  191. * @param {module:engine/model/range~Range} range Changed range.
  192. * @param {String} key Key of the attribute that has changed.
  193. * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
  194. * @param {*} newValue New attribute value or `null` if the attribute has been removed.
  195. */
  196. convertAttribute( range, key, oldValue, newValue ) {
  197. // Create a list with attributes to consume.
  198. const consumable = this._createConsumableForRange( range, `attribute:${ key }` );
  199. // Create a separate attribute event for each node in the range.
  200. for ( const value of range ) {
  201. const item = value.item;
  202. const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
  203. const data = {
  204. item,
  205. range: itemRange,
  206. attributeKey: key,
  207. attributeOldValue: oldValue,
  208. attributeNewValue: newValue
  209. };
  210. this._testAndFire( `attribute:${ key }`, data, consumable );
  211. }
  212. }
  213. /**
  214. * Starts model selection conversion.
  215. *
  216. * Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
  217. *
  218. * @fires selection
  219. * @fires selectionMarker
  220. * @fires selectionAttribute
  221. * @param {module:engine/model/selection~Selection} selection Selection to convert.
  222. */
  223. convertSelection( selection ) {
  224. const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
  225. const consumable = this._createSelectionConsumable( selection, markers );
  226. this.fire( 'selection', { selection }, consumable, this.conversionApi );
  227. for ( const marker of markers ) {
  228. const markerRange = marker.getRange();
  229. if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
  230. continue;
  231. }
  232. const data = {
  233. selection,
  234. markerName: marker.name,
  235. markerRange
  236. };
  237. if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
  238. this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi );
  239. }
  240. }
  241. for ( const key of selection.getAttributeKeys() ) {
  242. const data = {
  243. selection,
  244. key,
  245. value: selection.getAttribute( key )
  246. };
  247. // Do not fire event if the attribute has been consumed.
  248. if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
  249. this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
  250. }
  251. }
  252. }
  253. /**
  254. * Converts added marker. Fires {@link #event:addMarker addMarker} event for each item
  255. * in marker's range. If range is collapsed single event is dispatched. See event description for more details.
  256. *
  257. * @fires addMarker
  258. * @param {String} markerName Marker name.
  259. * @param {module:engine/model/range~Range} markerRange Marker range.
  260. */
  261. convertMarkerAdd( markerName, markerRange ) {
  262. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  263. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  264. return;
  265. }
  266. // In markers' case, event name == consumable name.
  267. const eventName = 'addMarker:' + markerName;
  268. // When range is collapsed - fire single event with collapsed range in consumable.
  269. if ( markerRange.isCollapsed ) {
  270. const consumable = new Consumable();
  271. consumable.add( markerRange, eventName );
  272. this.fire( eventName, {
  273. markerName,
  274. markerRange
  275. }, consumable, this.conversionApi );
  276. return;
  277. }
  278. // Create consumable for each item in range.
  279. const consumable = this._createConsumableForRange( markerRange, eventName );
  280. // Create separate event for each node in the range.
  281. for ( const item of markerRange.getItems() ) {
  282. // Do not fire event for already consumed items.
  283. if ( !consumable.test( item, eventName ) ) {
  284. continue;
  285. }
  286. const data = { item, range: Range.createOn( item ), markerName, markerRange };
  287. this.fire( eventName, data, consumable, this.conversionApi );
  288. }
  289. }
  290. /**
  291. * Fires conversion of marker removal. Fires {@link #event:removeMarker removeMarker} event with provided data.
  292. *
  293. * @fires removeMarker
  294. * @param {String} markerName Marker name.
  295. * @param {module:engine/model/range~Range} markerRange Marker range.
  296. */
  297. convertMarkerRemove( markerName, markerRange ) {
  298. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  299. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  300. return;
  301. }
  302. this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
  303. }
  304. /**
  305. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range,
  306. * assuming that the range has just been inserted to the model.
  307. *
  308. * @private
  309. * @param {module:engine/model/range~Range} range Inserted range.
  310. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  311. */
  312. _createInsertConsumable( range ) {
  313. const consumable = new Consumable();
  314. for ( const value of range ) {
  315. const item = value.item;
  316. consumable.add( item, 'insert' );
  317. for ( const key of item.getAttributeKeys() ) {
  318. consumable.add( item, 'attribute:' + key );
  319. }
  320. }
  321. return consumable;
  322. }
  323. /**
  324. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for given range.
  325. *
  326. * @private
  327. * @param {module:engine/model/range~Range} range Affected range.
  328. * @param {String} type Consumable type.
  329. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  330. */
  331. _createConsumableForRange( range, type ) {
  332. const consumable = new Consumable();
  333. for ( const item of range.getItems() ) {
  334. consumable.add( item, type );
  335. }
  336. return consumable;
  337. }
  338. /**
  339. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
  340. *
  341. * @private
  342. * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
  343. * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
  344. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  345. */
  346. _createSelectionConsumable( selection, markers ) {
  347. const consumable = new Consumable();
  348. consumable.add( selection, 'selection' );
  349. for ( const marker of markers ) {
  350. consumable.add( selection, 'selectionMarker:' + marker.name );
  351. }
  352. for ( const key of selection.getAttributeKeys() ) {
  353. consumable.add( selection, 'selectionAttribute:' + key );
  354. }
  355. return consumable;
  356. }
  357. /**
  358. * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
  359. *
  360. * @private
  361. * @fires insert
  362. * @fires attribute
  363. * @param {String} type Event type.
  364. * @param {Object} data Event data.
  365. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  366. */
  367. _testAndFire( type, data, consumable ) {
  368. if ( !consumable.test( data.item, type ) ) {
  369. // Do not fire event if the item was consumed.
  370. return;
  371. }
  372. const name = data.item.name || '$text';
  373. this.fire( type + ':' + name, data, consumable, this.conversionApi );
  374. }
  375. /**
  376. * Fired for inserted nodes.
  377. *
  378. * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
  379. * `insert:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been inserted,
  380. * or {@link module:engine/model/element~Element#name name} of inserted element.
  381. *
  382. * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
  383. *
  384. * @event insert
  385. * @param {Object} data Additional information about the change.
  386. * @param {module:engine/model/item~Item} data.item Inserted item.
  387. * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
  388. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  389. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  390. */
  391. /**
  392. * Fired for removed nodes.
  393. *
  394. * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
  395. * `remove:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been removed,
  396. * or the {@link module:engine/model/element~Element#name name} of removed element.
  397. *
  398. * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
  399. *
  400. * @event remove
  401. * @param {Object} data Additional information about the change.
  402. * @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed.
  403. * @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard
  404. * graveyard root}).
  405. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  406. */
  407. /**
  408. * Fired when attribute has been added/changed/removed from a node.
  409. *
  410. * `attribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  411. * `attribute:attributeKey:name`. `attributeKey` is the key of added/changed/removed attribute.
  412. * `name` is either `'$text'` if change was on {@link module:engine/model/text~Text a text node},
  413. * or the {@link module:engine/model/element~Element#name name} of element which attribute has changed.
  414. *
  415. * This way listeners can either listen to a general `attribute:bold` event or specific event (for example `attribute:src:image`).
  416. *
  417. * @event attribute
  418. * @param {Object} data Additional information about the change.
  419. * @param {module:engine/model/item~Item} data.item Changed item.
  420. * @param {module:engine/model/range~Range} data.range Range spanning over changed item.
  421. * @param {String} data.attributeKey Attribute key.
  422. * @param {*} data.attributeOldValue Attribute value before the change.
  423. * @param {*} data.attributeNewValue New attribute value.
  424. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  425. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  426. */
  427. /**
  428. * Fired for {@link module:engine/model/selection~Selection selection} changes.
  429. *
  430. * @event selection
  431. * @param {module:engine/model/selection~Selection} selection Selection that is converted.
  432. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  433. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  434. */
  435. /**
  436. * Fired for {@link module:engine/model/selection~Selection selection} attributes changes.
  437. *
  438. * `selectionAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  439. * `selectionAttribute:attributeKey`. `attributeKey` is the key of selection attribute. This way it is possible to listen to
  440. * certain attribute, i.e. `selectionAttribute:bold`.
  441. *
  442. * @event selectionAttribute
  443. * @param {Object} data Additional information about the change.
  444. * @param {module:engine/model/selection~Selection} data.selection Selection that is converted.
  445. * @param {String} data.attributeKey Key of changed attribute.
  446. * @param {*} data.attributeValue Value of changed attribute.
  447. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  448. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  449. */
  450. /**
  451. * Fired for markers containing {@link module:engine/model/selection~Selection selection}.
  452. *
  453. * `selectionMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  454. * `selectionMarker:markerName`. `markerName` is the name of the marker containing selection. This way it is possible to listen to
  455. * certain marker, i.e. `selectionAttribute:highlight`.
  456. *
  457. * @event selectionMarker
  458. * @param {Object} data Additional information about the change.
  459. * @param {module:engine/model/selection~Selection} data.selection Selection that is converted.
  460. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  461. * @param {String} data.markerName Marker name.
  462. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  463. */
  464. /**
  465. * Fired when a new marker is added to the model.
  466. *
  467. * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  468. * `addMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  469. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
  470. * `addMarker:foo:bar` events.
  471. *
  472. * If the marker range is not collapsed:
  473. * * the event is fired for each item in the marker range one by one,
  474. * * consumables object includes each item of the marker range and the consumable value is same as event name.
  475. *
  476. * If the marker range is collapsed:
  477. * * there is only one event,
  478. * * consumables object includes marker range with event name.
  479. *
  480. * @event addMarker
  481. * @param {Object} data Additional information about the change.
  482. * @param {module:engine/model/item~Item} data.item Item inside the new marker.
  483. * @param {module:engine/model/range~Range} [data.range] Range spanning over converted item. Available only if
  484. * the marker range was not collapsed.
  485. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  486. * @param {String} data.markerName Marker name.
  487. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  488. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  489. */
  490. /**
  491. * Fired when marker is removed from the model.
  492. *
  493. * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  494. * `removeMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  495. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
  496. * `removeMarker:foo:bar` events.
  497. *
  498. * @event removeMarker
  499. * @param {Object} data Additional information about the change.
  500. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  501. * @param {String} data.markerName Marker name.
  502. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  503. */
  504. }
  505. mix( ModelConversionDispatcher, EmitterMixin );
  506. // Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
  507. // converted if they happen inside an element with custom conversion method.
  508. //
  509. // @param {module:engine/model/position~Position} modelPosition
  510. // @param {module:engine/model/markercollection~Marker} marker
  511. // @param {module:engine/conversion/mapper~Mapper} mapper
  512. // @returns {Boolean}
  513. function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
  514. const range = marker.getRange();
  515. const ancestors = Array.from( modelPosition.getAncestors() );
  516. ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
  517. ancestors.reverse();
  518. const hasCustomHandling = ancestors.some( element => {
  519. if ( range.containsItem( element ) ) {
  520. const viewElement = mapper.toViewElement( element );
  521. return !!viewElement.getCustomProperty( 'addHighlight' );
  522. }
  523. } );
  524. return !hasCustomHandling;
  525. }