downcastdispatcher.js 30 KB

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