downcastdispatcher.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  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. // 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. const changes = differ.getChanges();
  130. if ( changes.length ) {
  131. // @if CK_DEBUG // console.log( `convertChanges() size: ${ changes.length }` );
  132. }
  133. // Convert changes that happened on model tree.
  134. for ( const entry of changes ) {
  135. // @if CK_DEBUG // console.log( `differ: ${ entry.type }` );
  136. if ( entry.type == 'insert' ) {
  137. this.convertInsert( Range._createFromPositionAndShift( entry.position, entry.length ), writer );
  138. } else if ( entry.type == 'remove' ) {
  139. this.convertRemove( entry.position, entry.length, entry.name, writer );
  140. } else if ( entry.type == 'refresh' ) {
  141. // @if CK_DEBUG console.warn( 'convert refresh' );
  142. this.convertRefresh( Range._createFromPositionAndShift( entry.position, entry.length ), writer );
  143. } else if ( entry.type == 'attribute' ) {
  144. this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer );
  145. } else {
  146. // todo warning
  147. }
  148. }
  149. for ( const markerName of this.conversionApi.mapper.flushUnboundMarkerNames() ) {
  150. const markerRange = markers.get( markerName ).getRange();
  151. this.convertMarkerRemove( markerName, markerRange, writer );
  152. this.convertMarkerAdd( markerName, markerRange, writer );
  153. }
  154. // After the view is updated, convert markers which have changed.
  155. for ( const change of differ.getMarkersToAdd() ) {
  156. this.convertMarkerAdd( change.name, change.range, writer );
  157. }
  158. }
  159. mapRefreshEvents( modelName, events = [] ) {
  160. for ( const eventName of events ) {
  161. this._map.set( eventName, modelName );
  162. }
  163. }
  164. pocCheckChangesForRefresh( differ ) {
  165. const changes = differ.getChanges();
  166. const found = [ ...changes ]
  167. .filter( ( { type } ) => type === 'attribute' || type === 'insert' || type === 'remove' )
  168. .map( entry => {
  169. const { range, position, type } = entry;
  170. const element = range && range.start.nodeAfter || position && position.parent;
  171. let eventName;
  172. if ( type === 'attribute' ) {
  173. // TODO: enhance event name retrieval.
  174. eventName = `attribute:${ entry.attributeKey }:${ element && element.name }`;
  175. } else {
  176. eventName = `${ type }:${ entry.name }`;
  177. }
  178. if ( this._map.has( eventName ) ) {
  179. const expectedElement = this._map.get( eventName );
  180. return element.is( 'element', expectedElement ) ? element : element.findAncestor( expectedElement );
  181. }
  182. } )
  183. .filter( element => !!element );
  184. const elementsToRefresh = new Set( found );
  185. [ ...elementsToRefresh.values() ].forEach( box => differ._pocRefreshItem( box ) );
  186. }
  187. /**
  188. * Starts a conversion of a range insertion.
  189. *
  190. * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
  191. * {@link #event:attribute `attribute` event is fired}.
  192. *
  193. * @fires insert
  194. * @fires attribute
  195. * @param {module:engine/model/range~Range} range The inserted range.
  196. * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
  197. */
  198. convertInsert( range, writer ) {
  199. this.conversionApi.writer = writer;
  200. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  201. this.conversionApi.consumable = this._createInsertConsumable( range );
  202. // Fire a separate insert event for each node and text fragment contained in the range.
  203. for ( const value of range ) {
  204. const item = value.item;
  205. const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
  206. const data = {
  207. item,
  208. range: itemRange
  209. };
  210. this._testAndFire( 'insert', data );
  211. // Fire a separate addAttribute event for each attribute that was set on inserted items.
  212. // This is important because most attributes converters will listen only to add/change/removeAttribute events.
  213. // If we would not add this part, attributes on inserted nodes would not be converted.
  214. for ( const key of item.getAttributeKeys() ) {
  215. data.attributeKey = key;
  216. data.attributeOldValue = null;
  217. data.attributeNewValue = item.getAttribute( key );
  218. this._testAndFire( `attribute:${ key }`, data );
  219. }
  220. }
  221. this._clearConversionApi();
  222. }
  223. /**
  224. * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
  225. *
  226. * @param {module:engine/model/position~Position} position Position from which node was removed.
  227. * @param {Number} length Offset size of removed node.
  228. * @param {String} name Name of removed node.
  229. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  230. */
  231. convertRemove( position, length, name, writer ) {
  232. this.conversionApi.writer = writer;
  233. this.fire( 'remove:' + name, { position, length }, this.conversionApi );
  234. this._clearConversionApi();
  235. }
  236. /**
  237. * Starts conversion of attribute change on given `range`.
  238. *
  239. * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
  240. *
  241. * @fires attribute
  242. * @param {module:engine/model/range~Range} range Changed range.
  243. * @param {String} key Key of the attribute that has changed.
  244. * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
  245. * @param {*} newValue New attribute value or `null` if the attribute has been removed.
  246. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  247. */
  248. convertAttribute( range, key, oldValue, newValue, writer ) {
  249. this.conversionApi.writer = writer;
  250. // Create a list with attributes to consume.
  251. this.conversionApi.consumable = this._createConsumableForRange( range, `attribute:${ key }` );
  252. // Create a separate attribute event for each node in the range.
  253. for ( const value of range ) {
  254. const item = value.item;
  255. const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
  256. const data = {
  257. item,
  258. range: itemRange,
  259. attributeKey: key,
  260. attributeOldValue: oldValue,
  261. attributeNewValue: newValue
  262. };
  263. this._testAndFire( `attribute:${ key }`, data );
  264. }
  265. this._clearConversionApi();
  266. }
  267. convertRefresh( range, writer ) {
  268. this.conversionApi.writer = writer;
  269. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  270. this.conversionApi.consumable = this._createInsertConsumable( range );
  271. for ( const value of range ) {
  272. const item = value.item;
  273. const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
  274. const data = {
  275. item,
  276. range: itemRange,
  277. isRefresh: true
  278. };
  279. this._testAndFire( 'insert', data );
  280. }
  281. this._clearConversionApi();
  282. }
  283. /**
  284. * Starts model selection conversion.
  285. *
  286. * Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
  287. *
  288. * @fires selection
  289. * @fires addMarker
  290. * @fires attribute
  291. * @param {module:engine/model/selection~Selection} selection Selection to convert.
  292. * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with converted model.
  293. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  294. */
  295. convertSelection( selection, markers, writer ) {
  296. const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
  297. this.conversionApi.writer = writer;
  298. this.conversionApi.consumable = this._createSelectionConsumable( selection, markersAtSelection );
  299. this.fire( 'selection', { selection }, this.conversionApi );
  300. if ( !selection.isCollapsed ) {
  301. return;
  302. }
  303. for ( const marker of markersAtSelection ) {
  304. const markerRange = marker.getRange();
  305. if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
  306. continue;
  307. }
  308. const data = {
  309. item: selection,
  310. markerName: marker.name,
  311. markerRange
  312. };
  313. if ( this.conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
  314. this.fire( 'addMarker:' + marker.name, data, this.conversionApi );
  315. }
  316. }
  317. for ( const key of selection.getAttributeKeys() ) {
  318. const data = {
  319. item: selection,
  320. range: selection.getFirstRange(),
  321. attributeKey: key,
  322. attributeOldValue: null,
  323. attributeNewValue: selection.getAttribute( key )
  324. };
  325. // Do not fire event if the attribute has been consumed.
  326. if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
  327. this.fire( 'attribute:' + data.attributeKey + ':$text', data, this.conversionApi );
  328. }
  329. }
  330. this._clearConversionApi();
  331. }
  332. /**
  333. * Converts added marker. Fires {@link #event:addMarker addMarker} event for each item
  334. * in marker's range. If range is collapsed single event is dispatched. See event description for more details.
  335. *
  336. * @fires addMarker
  337. * @param {String} markerName Marker name.
  338. * @param {module:engine/model/range~Range} markerRange Marker range.
  339. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  340. */
  341. convertMarkerAdd( markerName, markerRange, writer ) {
  342. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  343. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  344. return;
  345. }
  346. this.conversionApi.writer = writer;
  347. // In markers' case, event name == consumable name.
  348. const eventName = 'addMarker:' + markerName;
  349. //
  350. // First, fire an event for the whole marker.
  351. //
  352. const consumable = new Consumable();
  353. consumable.add( markerRange, eventName );
  354. this.conversionApi.consumable = consumable;
  355. this.fire( eventName, { markerName, markerRange }, this.conversionApi );
  356. //
  357. // Do not fire events for each item inside the range if the range got consumed.
  358. //
  359. if ( !consumable.test( markerRange, eventName ) ) {
  360. return;
  361. }
  362. //
  363. // Then, fire an event for each item inside the marker range.
  364. //
  365. this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
  366. for ( const item of markerRange.getItems() ) {
  367. // Do not fire event for already consumed items.
  368. if ( !this.conversionApi.consumable.test( item, eventName ) ) {
  369. continue;
  370. }
  371. const data = { item, range: Range._createOn( item ), markerName, markerRange };
  372. this.fire( eventName, data, this.conversionApi );
  373. }
  374. this._clearConversionApi();
  375. }
  376. /**
  377. * Fires conversion of marker removal. Fires {@link #event:removeMarker removeMarker} event with provided data.
  378. *
  379. * @fires removeMarker
  380. * @param {String} markerName Marker name.
  381. * @param {module:engine/model/range~Range} markerRange Marker range.
  382. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  383. */
  384. convertMarkerRemove( markerName, markerRange, writer ) {
  385. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  386. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  387. return;
  388. }
  389. this.conversionApi.writer = writer;
  390. this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
  391. this._clearConversionApi();
  392. }
  393. /**
  394. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range,
  395. * assuming that the range has just been inserted to the model.
  396. *
  397. * @private
  398. * @param {module:engine/model/range~Range} range Inserted range.
  399. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  400. */
  401. _createInsertConsumable( range ) {
  402. const consumable = new Consumable();
  403. for ( const value of range ) {
  404. const item = value.item;
  405. consumable.add( item, 'insert' );
  406. for ( const key of item.getAttributeKeys() ) {
  407. consumable.add( item, 'attribute:' + key );
  408. }
  409. }
  410. return consumable;
  411. }
  412. /**
  413. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for given range.
  414. *
  415. * @private
  416. * @param {module:engine/model/range~Range} range Affected range.
  417. * @param {String} type Consumable type.
  418. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  419. */
  420. _createConsumableForRange( range, type ) {
  421. const consumable = new Consumable();
  422. for ( const item of range.getItems() ) {
  423. consumable.add( item, type );
  424. }
  425. return consumable;
  426. }
  427. /**
  428. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
  429. *
  430. * @private
  431. * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
  432. * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
  433. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  434. */
  435. _createSelectionConsumable( selection, markers ) {
  436. const consumable = new Consumable();
  437. consumable.add( selection, 'selection' );
  438. for ( const marker of markers ) {
  439. consumable.add( selection, 'addMarker:' + marker.name );
  440. }
  441. for ( const key of selection.getAttributeKeys() ) {
  442. consumable.add( selection, 'attribute:' + key );
  443. }
  444. return consumable;
  445. }
  446. /**
  447. * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
  448. *
  449. * @private
  450. * @fires insert
  451. * @fires attribute
  452. * @param {String} type Event type.
  453. * @param {Object} data Event data.
  454. */
  455. _testAndFire( type, data ) {
  456. if ( !this.conversionApi.consumable.test( data.item, type ) ) {
  457. // Do not fire event if the item was consumed.
  458. return;
  459. }
  460. const name = data.item.name || '$text';
  461. this.fire( type + ':' + name, data, this.conversionApi );
  462. }
  463. /**
  464. * Clears conversion API object.
  465. *
  466. * @private
  467. */
  468. _clearConversionApi() {
  469. delete this.conversionApi.writer;
  470. delete this.conversionApi.consumable;
  471. }
  472. /**
  473. * Fired for inserted nodes.
  474. *
  475. * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
  476. * `insert:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been inserted,
  477. * or {@link module:engine/model/element~Element#name name} of inserted element.
  478. *
  479. * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
  480. *
  481. * @event insert
  482. * @param {Object} data Additional information about the change.
  483. * @param {module:engine/model/item~Item} data.item Inserted item.
  484. * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
  485. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  486. * to be used by callback, passed in `DowncastDispatcher` constructor.
  487. */
  488. /**
  489. * Fired for removed nodes.
  490. *
  491. * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
  492. * `remove:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been removed,
  493. * or the {@link module:engine/model/element~Element#name name} of removed element.
  494. *
  495. * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
  496. *
  497. * @event remove
  498. * @param {Object} data Additional information about the change.
  499. * @param {module:engine/model/position~Position} data.position Position from which the node has been removed.
  500. * @param {Number} data.length Offset size of the removed node.
  501. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  502. * to be used by callback, passed in `DowncastDispatcher` constructor.
  503. */
  504. /**
  505. * Fired in the following cases:
  506. *
  507. * * when an attribute has been added, changed, or removed from a node,
  508. * * when a node with an attribute is inserted,
  509. * * when collapsed model selection attribute is converted.
  510. *
  511. * `attribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  512. * `attribute:attributeKey:name`. `attributeKey` is the key of added/changed/removed attribute.
  513. * `name` is either `'$text'` if change was on {@link module:engine/model/text~Text a text node},
  514. * or the {@link module:engine/model/element~Element#name name} of element which attribute has changed.
  515. *
  516. * This way listeners can either listen to a general `attribute:bold` event or specific event (for example `attribute:src:image`).
  517. *
  518. * @event attribute
  519. * @param {Object} data Additional information about the change.
  520. * @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} data.item Changed item
  521. * or converted selection.
  522. * @param {module:engine/model/range~Range} data.range Range spanning over changed item or selection range.
  523. * @param {String} data.attributeKey Attribute key.
  524. * @param {*} data.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
  525. * @param {*} data.attributeNewValue New attribute value.
  526. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  527. * to be used by callback, passed in `DowncastDispatcher` constructor.
  528. */
  529. /**
  530. * Fired for {@link module:engine/model/selection~Selection selection} changes.
  531. *
  532. * @event selection
  533. * @param {module:engine/model/selection~Selection} selection Selection that is converted.
  534. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  535. * to be used by callback, passed in `DowncastDispatcher` constructor.
  536. */
  537. /**
  538. * Fired when a new marker is added to the model. Also fired when collapsed model selection that is inside a marker is converted.
  539. *
  540. * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  541. * `addMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  542. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
  543. * `addMarker:foo:bar` events.
  544. *
  545. * If the marker range is not collapsed:
  546. *
  547. * * the event is fired for each item in the marker range one by one,
  548. * * `conversionApi.consumable` includes each item of the marker range and the consumable value is same as event name.
  549. *
  550. * If the marker range is collapsed:
  551. *
  552. * * there is only one event,
  553. * * `conversionApi.consumable` includes marker range with event name.
  554. *
  555. * If selection inside a marker is converted:
  556. *
  557. * * there is only one event,
  558. * * `conversionApi.consumable` includes selection instance with event name.
  559. *
  560. * @event addMarker
  561. * @param {Object} data Additional information about the change.
  562. * @param {module:engine/model/item~Item|module:engine/model/selection~Selection} data.item Item inside the new marker or
  563. * the selection that is being converted.
  564. * @param {module:engine/model/range~Range} [data.range] Range spanning over converted item. Available only in marker conversion, if
  565. * the marker range was not collapsed.
  566. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  567. * @param {String} data.markerName Marker name.
  568. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  569. * to be used by callback, passed in `DowncastDispatcher` constructor.
  570. */
  571. /**
  572. * Fired when marker is removed from the model.
  573. *
  574. * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  575. * `removeMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  576. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
  577. * `removeMarker:foo:bar` events.
  578. *
  579. * @event removeMarker
  580. * @param {Object} data Additional information about the change.
  581. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  582. * @param {String} data.markerName Marker name.
  583. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  584. * to be used by callback, passed in `DowncastDispatcher` constructor.
  585. */
  586. }
  587. mix( DowncastDispatcher, EmitterMixin );
  588. // Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
  589. // converted if they happen inside an element with custom conversion method.
  590. //
  591. // @param {module:engine/model/position~Position} modelPosition
  592. // @param {module:engine/model/markercollection~Marker} marker
  593. // @param {module:engine/conversion/mapper~Mapper} mapper
  594. // @returns {Boolean}
  595. function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
  596. const range = marker.getRange();
  597. const ancestors = Array.from( modelPosition.getAncestors() );
  598. ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
  599. ancestors.reverse();
  600. const hasCustomHandling = ancestors.some( element => {
  601. if ( range.containsItem( element ) ) {
  602. const viewElement = mapper.toViewElement( element );
  603. return !!viewElement.getCustomProperty( 'addHighlight' );
  604. }
  605. } );
  606. return !hasCustomHandling;
  607. }
  608. /**
  609. * Conversion interface that is registered for given {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
  610. * and is passed as one of parameters when {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher dispatcher}
  611. * fires its events.
  612. *
  613. * @interface module:engine/conversion/downcastdispatcher~DowncastConversionApi
  614. */
  615. /**
  616. * The {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} instance.
  617. *
  618. * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #dispatcher
  619. */
  620. /**
  621. * Stores information about what parts of processed model item are still waiting to be handled. After a piece of model item
  622. * was converted, appropriate consumable value should be {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
  623. *
  624. * @member {module:engine/conversion/modelconsumable~ModelConsumable} #consumable
  625. */
  626. /**
  627. * The {@link module:engine/conversion/mapper~Mapper} instance.
  628. *
  629. * @member {module:engine/conversion/mapper~Mapper} #mapper
  630. */
  631. /**
  632. * The {@link module:engine/model/schema~Schema} instance set for the model that is downcast.
  633. *
  634. * @member {module:engine/model/schema~Schema} #schema
  635. */
  636. /**
  637. * The {@link module:engine/view/downcastwriter~DowncastWriter} instance used to manipulate data during conversion.
  638. *
  639. * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
  640. */
  641. /**
  642. * An object with an additional configuration which can be used during conversion process. Available only for data downcast conversion.
  643. *
  644. * @member {Object} #options
  645. */