downcastdispatcher.js 35 KB

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