downcastdispatcher.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  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, { getNodeAfterPosition, getTextNodeAtPosition } 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._reconversionEventsMapping = 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. * Starts a conversion of a range insertion.
  168. *
  169. * For each node in the range, {@link #event:insert `insert` event is fired}. For each attribute on each node,
  170. * {@link #event:attribute `attribute` event is fired}.
  171. *
  172. * @fires insert
  173. * @fires attribute
  174. * @param {module:engine/model/range~Range} range The inserted range.
  175. * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
  176. */
  177. convertInsert( range, writer ) {
  178. this.conversionApi.writer = writer;
  179. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  180. this.conversionApi.consumable = this._createInsertConsumable( range );
  181. // Fire a separate insert event for each node and text fragment contained in the range.
  182. for ( const data of Array.from( range ).map( walkerValueToEventData ) ) {
  183. this._convertInsertWithAttributes( data );
  184. }
  185. this._clearConversionApi();
  186. }
  187. /**
  188. * Fires conversion of a single node removal. Fires {@link #event:remove remove event} with provided data.
  189. *
  190. * @param {module:engine/model/position~Position} position Position from which node was removed.
  191. * @param {Number} length Offset size of removed node.
  192. * @param {String} name Name of removed node.
  193. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  194. */
  195. convertRemove( position, length, name, writer ) {
  196. this.conversionApi.writer = writer;
  197. this.fire( 'remove:' + name, { position, length }, this.conversionApi );
  198. this._clearConversionApi();
  199. }
  200. /**
  201. * Starts conversion of attribute change on given `range`.
  202. *
  203. * For each node in the given `range`, {@link #event:attribute attribute event} is fired with the passed data.
  204. *
  205. * @fires attribute
  206. * @param {module:engine/model/range~Range} range Changed range.
  207. * @param {String} key Key of the attribute that has changed.
  208. * @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
  209. * @param {*} newValue New attribute value or `null` if the attribute has been removed.
  210. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  211. */
  212. convertAttribute( range, key, oldValue, newValue, writer ) {
  213. this.conversionApi.writer = writer;
  214. // Create a list with attributes to consume.
  215. this.conversionApi.consumable = this._createConsumableForRange( range, `attribute:${ key }` );
  216. // Create a separate attribute event for each node in the range.
  217. for ( const value of range ) {
  218. const item = value.item;
  219. const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
  220. const data = {
  221. item,
  222. range: itemRange,
  223. attributeKey: key,
  224. attributeOldValue: oldValue,
  225. attributeNewValue: newValue
  226. };
  227. this._testAndFire( `attribute:${ key }`, data );
  228. }
  229. this._clearConversionApi();
  230. }
  231. /**
  232. * Starts a reconversion of an element. It will:
  233. *
  234. * * Fire a {@link #event:insert `insert` event} for the element to reconvert.
  235. * * Fire an {@link #event:attribute `attribute` event} for element attributes.
  236. *
  237. * This will not reconvert children of the element if they have existing (already converted) views. For newly inserted child elements
  238. * it will behave the same as {@link #convertInsert}.
  239. *
  240. * Element reconversion is defined by a `triggerBy` configuration for
  241. * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
  242. *
  243. * @fires insert
  244. * @fires attribute
  245. * @param {module:engine/model/element~Element} element The element to be reconverted.
  246. * @param {module:engine/view/downcastwriter~DowncastWriter} writer The view writer that should be used to modify the view document.
  247. */
  248. reconvertElement( element, writer ) {
  249. const elementRange = Range._createOn( element );
  250. this.conversionApi.writer = writer;
  251. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  252. this.conversionApi.consumable = this._createInsertConsumable( elementRange );
  253. const mapper = this.conversionApi.mapper;
  254. const currentView = mapper.toViewElement( element );
  255. // Remove the old view but do not remove mapper mappings - those will be used to revive existing elements.
  256. writer.remove( currentView );
  257. // Convert the element - without converting children.
  258. this._convertInsertWithAttributes( {
  259. item: element,
  260. range: elementRange
  261. } );
  262. const convertedViewElement = mapper.toViewElement( element );
  263. // Iterate over children of reconverted element in order to...
  264. for ( const value of Range._createIn( element ) ) {
  265. const { item } = value;
  266. const view = elementOrTextProxyToView( item, mapper );
  267. // ...either bring back previously converted view...
  268. if ( view ) {
  269. // Do not move views that are already in converted element - those might be created by the main element converter in case
  270. // when main element converts also its direct children.
  271. if ( view.root !== convertedViewElement.root ) {
  272. writer.move(
  273. writer.createRangeOn( view ),
  274. mapper.toViewPosition( Position._createBefore( item ) )
  275. );
  276. }
  277. }
  278. // ... or by converting newly inserted elements.
  279. else {
  280. this._convertInsertWithAttributes( walkerValueToEventData( value ) );
  281. }
  282. }
  283. this._clearConversionApi();
  284. }
  285. /**
  286. * Starts model selection conversion.
  287. *
  288. * Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
  289. *
  290. * @fires selection
  291. * @fires addMarker
  292. * @fires attribute
  293. * @param {module:engine/model/selection~Selection} selection Selection to convert.
  294. * @param {module:engine/model/markercollection~MarkerCollection} markers Markers connected with converted model.
  295. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  296. */
  297. convertSelection( selection, markers, writer ) {
  298. const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) );
  299. this.conversionApi.writer = writer;
  300. this.conversionApi.consumable = this._createSelectionConsumable( selection, markersAtSelection );
  301. this.fire( 'selection', { selection }, this.conversionApi );
  302. if ( !selection.isCollapsed ) {
  303. return;
  304. }
  305. for ( const marker of markersAtSelection ) {
  306. const markerRange = marker.getRange();
  307. if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
  308. continue;
  309. }
  310. const data = {
  311. item: selection,
  312. markerName: marker.name,
  313. markerRange
  314. };
  315. if ( this.conversionApi.consumable.test( selection, 'addMarker:' + marker.name ) ) {
  316. this.fire( 'addMarker:' + marker.name, data, this.conversionApi );
  317. }
  318. }
  319. for ( const key of selection.getAttributeKeys() ) {
  320. const data = {
  321. item: selection,
  322. range: selection.getFirstRange(),
  323. attributeKey: key,
  324. attributeOldValue: null,
  325. attributeNewValue: selection.getAttribute( key )
  326. };
  327. // Do not fire event if the attribute has been consumed.
  328. if ( this.conversionApi.consumable.test( selection, 'attribute:' + data.attributeKey ) ) {
  329. this.fire( 'attribute:' + data.attributeKey + ':$text', data, this.conversionApi );
  330. }
  331. }
  332. this._clearConversionApi();
  333. }
  334. /**
  335. * Converts added marker. Fires {@link #event:addMarker addMarker} event for each item
  336. * in marker's range. If range is collapsed single event is dispatched. See event description for more details.
  337. *
  338. * @fires addMarker
  339. * @param {String} markerName Marker name.
  340. * @param {module:engine/model/range~Range} markerRange Marker range.
  341. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  342. */
  343. convertMarkerAdd( markerName, markerRange, writer ) {
  344. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  345. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  346. return;
  347. }
  348. this.conversionApi.writer = writer;
  349. // In markers' case, event name == consumable name.
  350. const eventName = 'addMarker:' + markerName;
  351. //
  352. // First, fire an event for the whole marker.
  353. //
  354. const consumable = new Consumable();
  355. consumable.add( markerRange, eventName );
  356. this.conversionApi.consumable = consumable;
  357. this.fire( eventName, { markerName, markerRange }, this.conversionApi );
  358. //
  359. // Do not fire events for each item inside the range if the range got consumed.
  360. //
  361. if ( !consumable.test( markerRange, eventName ) ) {
  362. return;
  363. }
  364. //
  365. // Then, fire an event for each item inside the marker range.
  366. //
  367. this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );
  368. for ( const item of markerRange.getItems() ) {
  369. // Do not fire event for already consumed items.
  370. if ( !this.conversionApi.consumable.test( item, eventName ) ) {
  371. continue;
  372. }
  373. const data = { item, range: Range._createOn( item ), markerName, markerRange };
  374. this.fire( eventName, data, this.conversionApi );
  375. }
  376. this._clearConversionApi();
  377. }
  378. /**
  379. * Fires conversion of marker removal. Fires {@link #event:removeMarker removeMarker} event with provided data.
  380. *
  381. * @fires removeMarker
  382. * @param {String} markerName Marker name.
  383. * @param {module:engine/model/range~Range} markerRange Marker range.
  384. * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that should be used to modify view document.
  385. */
  386. convertMarkerRemove( markerName, markerRange, writer ) {
  387. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  388. if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) {
  389. return;
  390. }
  391. this.conversionApi.writer = writer;
  392. this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi );
  393. this._clearConversionApi();
  394. }
  395. /**
  396. * Maps model element "insert" reconversion for given event names. The event names must be fully specified:
  397. *
  398. * * For "attribute" change event it should include main element name, ie: `'attribute:attributeName:elementName'`.
  399. * * For child nodes change events, those should use child event name as well, ie:
  400. * * For adding a node: `'insert:childElementName'`.
  401. * * For removing a node: `'remove:childElementName'`.
  402. *
  403. * **Note**: This method should not be used directly. A reconversion is defined by `triggerBy` configuration of the `elementToElement()`
  404. * conversion helper.
  405. *
  406. * @protected
  407. * @param {String} modelName Main model element name for which events will trigger reconversion.
  408. * @param {String} eventName Name of an event that would trigger conversion for given model element.
  409. */
  410. _mapReconversionTriggerEvent( modelName, eventName ) {
  411. this._reconversionEventsMapping.set( eventName, modelName );
  412. }
  413. /**
  414. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range,
  415. * assuming that the range has just been inserted to the model.
  416. *
  417. * @private
  418. * @param {module:engine/model/range~Range} range Inserted range.
  419. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  420. */
  421. _createInsertConsumable( range ) {
  422. const consumable = new Consumable();
  423. for ( const value of range ) {
  424. const item = value.item;
  425. consumable.add( item, 'insert' );
  426. for ( const key of item.getAttributeKeys() ) {
  427. consumable.add( item, 'attribute:' + key );
  428. }
  429. }
  430. return consumable;
  431. }
  432. /**
  433. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume for given range.
  434. *
  435. * @private
  436. * @param {module:engine/model/range~Range} range Affected range.
  437. * @param {String} type Consumable type.
  438. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  439. */
  440. _createConsumableForRange( range, type ) {
  441. const consumable = new Consumable();
  442. for ( const item of range.getItems() ) {
  443. consumable.add( item, type );
  444. }
  445. return consumable;
  446. }
  447. /**
  448. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
  449. *
  450. * @private
  451. * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
  452. * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
  453. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  454. */
  455. _createSelectionConsumable( selection, markers ) {
  456. const consumable = new Consumable();
  457. consumable.add( selection, 'selection' );
  458. for ( const marker of markers ) {
  459. consumable.add( selection, 'addMarker:' + marker.name );
  460. }
  461. for ( const key of selection.getAttributeKeys() ) {
  462. consumable.add( selection, 'attribute:' + key );
  463. }
  464. return consumable;
  465. }
  466. /**
  467. * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
  468. *
  469. * @private
  470. * @fires insert
  471. * @fires attribute
  472. * @param {String} type Event type.
  473. * @param {Object} data Event data.
  474. */
  475. _testAndFire( type, data ) {
  476. if ( !this.conversionApi.consumable.test( data.item, type ) ) {
  477. // Do not fire event if the item was consumed.
  478. return;
  479. }
  480. this.fire( getEventName( type, data ), data, this.conversionApi );
  481. }
  482. /**
  483. * Clears conversion API object.
  484. *
  485. * @private
  486. */
  487. _clearConversionApi() {
  488. delete this.conversionApi.writer;
  489. delete this.conversionApi.consumable;
  490. }
  491. /**
  492. * Internal method for converting element insert. It will fire events for the inserted element and events for its attributes.
  493. *
  494. * @private
  495. * @fires insert
  496. * @fires attribute
  497. * @param {Object} data Event data.
  498. */
  499. _convertInsertWithAttributes( data ) {
  500. this._testAndFire( 'insert', data );
  501. // Fire a separate addAttribute event for each attribute that was set on inserted items.
  502. // This is important because most attributes converters will listen only to add/change/removeAttribute events.
  503. // If we would not add this part, attributes on inserted nodes would not be converted.
  504. for ( const key of data.item.getAttributeKeys() ) {
  505. data.attributeKey = key;
  506. data.attributeOldValue = null;
  507. data.attributeNewValue = data.item.getAttribute( key );
  508. this._testAndFire( `attribute:${ key }`, data );
  509. }
  510. }
  511. /**
  512. * Get changes without those that needs to be converted using {@link #reconvertElement} defined by a `triggerBy` configuration for
  513. * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
  514. *
  515. * @param {module:engine/model/differ~Differ} differ The differ object with buffered changes.
  516. * @returns {Array.<Object>}
  517. * @private
  518. */
  519. _mapChangesWithAutomaticReconversion( differ ) {
  520. const itemsToReconvert = new Set();
  521. const updated = [];
  522. for ( const entry of differ.getChanges() ) {
  523. const position = entry.position || entry.range.start;
  524. // Cached parent - just in case. See https://github.com/ckeditor/ckeditor5/issues/6579.
  525. const positionParent = position.parent;
  526. const textNode = getTextNodeAtPosition( position, positionParent );
  527. // Reconversion is done only on elements so skip text changes.
  528. if ( textNode ) {
  529. updated.push( entry );
  530. continue;
  531. }
  532. const element = entry.type === 'attribute' ? getNodeAfterPosition( position, positionParent, null ) : positionParent;
  533. // Case of text node set directly in root. For now used only in tests but can be possible when enabled in paragraph-like roots.
  534. // See: https://github.com/ckeditor/ckeditor5/issues/762.
  535. if ( element.is( '$text' ) ) {
  536. updated.push( entry );
  537. continue;
  538. }
  539. let eventName;
  540. if ( entry.type === 'attribute' ) {
  541. eventName = `attribute:${ entry.attributeKey }:${ element.name }`;
  542. } else {
  543. eventName = `${ entry.type }:${ entry.name }`;
  544. }
  545. if ( this._isReconvertTriggerEvent( eventName, element.name ) ) {
  546. if ( itemsToReconvert.has( element ) ) {
  547. // Element is already reconverted, so skip this change.
  548. continue;
  549. }
  550. itemsToReconvert.add( element );
  551. // Add special "reconvert" change.
  552. updated.push( { type: 'reconvert', element } );
  553. } else {
  554. updated.push( entry );
  555. }
  556. }
  557. return updated;
  558. }
  559. /**
  560. * Checks if resulting change should trigger element reconversion.
  561. *
  562. * Those are defined by a `triggerBy` configuration for
  563. * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#elementToElement `elementToElement()`} conversion helper.
  564. *
  565. * @private
  566. * @param {String} eventName Event name to check.
  567. * @param {String} elementName Element name to check.
  568. * @returns {Boolean}
  569. */
  570. _isReconvertTriggerEvent( eventName, elementName ) {
  571. return this._reconversionEventsMapping.get( eventName ) === elementName;
  572. }
  573. /**
  574. * Fired for inserted nodes.
  575. *
  576. * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
  577. * `insert:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been inserted,
  578. * or {@link module:engine/model/element~Element#name name} of inserted element.
  579. *
  580. * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
  581. *
  582. * @event insert
  583. * @param {Object} data Additional information about the change.
  584. * @param {module:engine/model/item~Item} data.item Inserted item.
  585. * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
  586. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  587. * to be used by callback, passed in `DowncastDispatcher` constructor.
  588. */
  589. /**
  590. * Fired for removed nodes.
  591. *
  592. * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
  593. * `remove:name`. `name` is either `'$text'`, when {@link module:engine/model/text~Text a text node} has been removed,
  594. * or the {@link module:engine/model/element~Element#name name} of removed element.
  595. *
  596. * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
  597. *
  598. * @event remove
  599. * @param {Object} data Additional information about the change.
  600. * @param {module:engine/model/position~Position} data.position Position from which the node has been removed.
  601. * @param {Number} data.length Offset size of the removed node.
  602. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  603. * to be used by callback, passed in `DowncastDispatcher` constructor.
  604. */
  605. /**
  606. * Fired in the following cases:
  607. *
  608. * * when an attribute has been added, changed, or removed from a node,
  609. * * when a node with an attribute is inserted,
  610. * * when collapsed model selection attribute is converted.
  611. *
  612. * `attribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  613. * `attribute:attributeKey:name`. `attributeKey` is the key of added/changed/removed attribute.
  614. * `name` is either `'$text'` if change was on {@link module:engine/model/text~Text a text node},
  615. * or the {@link module:engine/model/element~Element#name name} of element which attribute has changed.
  616. *
  617. * This way listeners can either listen to a general `attribute:bold` event or specific event (for example `attribute:src:image`).
  618. *
  619. * @event attribute
  620. * @param {Object} data Additional information about the change.
  621. * @param {module:engine/model/item~Item|module:engine/model/documentselection~DocumentSelection} data.item Changed item
  622. * or converted selection.
  623. * @param {module:engine/model/range~Range} data.range Range spanning over changed item or selection range.
  624. * @param {String} data.attributeKey Attribute key.
  625. * @param {*} data.attributeOldValue Attribute value before the change. This is `null` when selection attribute is converted.
  626. * @param {*} data.attributeNewValue New attribute value.
  627. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  628. * to be used by callback, passed in `DowncastDispatcher` constructor.
  629. */
  630. /**
  631. * Fired for {@link module:engine/model/selection~Selection selection} changes.
  632. *
  633. * @event selection
  634. * @param {module:engine/model/selection~Selection} selection Selection that is converted.
  635. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  636. * to be used by callback, passed in `DowncastDispatcher` constructor.
  637. */
  638. /**
  639. * Fired when a new marker is added to the model. Also fired when collapsed model selection that is inside a marker is converted.
  640. *
  641. * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  642. * `addMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  643. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
  644. * `addMarker:foo:bar` events.
  645. *
  646. * If the marker range is not collapsed:
  647. *
  648. * * the event is fired for each item in the marker range one by one,
  649. * * `conversionApi.consumable` includes each item of the marker range and the consumable value is same as event name.
  650. *
  651. * If the marker range is collapsed:
  652. *
  653. * * there is only one event,
  654. * * `conversionApi.consumable` includes marker range with event name.
  655. *
  656. * If selection inside a marker is converted:
  657. *
  658. * * there is only one event,
  659. * * `conversionApi.consumable` includes selection instance with event name.
  660. *
  661. * @event addMarker
  662. * @param {Object} data Additional information about the change.
  663. * @param {module:engine/model/item~Item|module:engine/model/selection~Selection} data.item Item inside the new marker or
  664. * the selection that is being converted.
  665. * @param {module:engine/model/range~Range} [data.range] Range spanning over converted item. Available only in marker conversion, if
  666. * the marker range was not collapsed.
  667. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  668. * @param {String} data.markerName Marker name.
  669. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  670. * to be used by callback, passed in `DowncastDispatcher` constructor.
  671. */
  672. /**
  673. * Fired when marker is removed from the model.
  674. *
  675. * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  676. * `removeMarker:markerName`. By specifying certain marker names, you can make the events even more gradual. For example,
  677. * if markers are named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
  678. * `removeMarker:foo:bar` events.
  679. *
  680. * @event removeMarker
  681. * @param {Object} data Additional information about the change.
  682. * @param {module:engine/model/range~Range} data.markerRange Marker range.
  683. * @param {String} data.markerName Marker name.
  684. * @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi Conversion interface
  685. * to be used by callback, passed in `DowncastDispatcher` constructor.
  686. */
  687. }
  688. mix( DowncastDispatcher, EmitterMixin );
  689. // Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
  690. // converted if they happen inside an element with custom conversion method.
  691. //
  692. // @param {module:engine/model/position~Position} modelPosition
  693. // @param {module:engine/model/markercollection~Marker} marker
  694. // @param {module:engine/conversion/mapper~Mapper} mapper
  695. // @returns {Boolean}
  696. function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
  697. const range = marker.getRange();
  698. const ancestors = Array.from( modelPosition.getAncestors() );
  699. ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
  700. ancestors.reverse();
  701. const hasCustomHandling = ancestors.some( element => {
  702. if ( range.containsItem( element ) ) {
  703. const viewElement = mapper.toViewElement( element );
  704. return !!viewElement.getCustomProperty( 'addHighlight' );
  705. }
  706. } );
  707. return !hasCustomHandling;
  708. }
  709. function getEventName( type, data ) {
  710. const name = data.item.name || '$text';
  711. return `${ type }:${ name }`;
  712. }
  713. function walkerValueToEventData( value ) {
  714. const item = value.item;
  715. const itemRange = Range._createFromPositionAndShift( value.previousPosition, value.length );
  716. return {
  717. item,
  718. range: itemRange
  719. };
  720. }
  721. function elementOrTextProxyToView( item, mapper ) {
  722. if ( item.is( 'textProxy' ) ) {
  723. const mappedPosition = mapper.toViewPosition( Position._createBefore( item ) );
  724. const positionParent = mappedPosition.parent;
  725. return positionParent.is( '$text' ) ? positionParent : null;
  726. }
  727. return mapper.toViewElement( item );
  728. }
  729. /**
  730. * Conversion interface that is registered for given {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}
  731. * and is passed as one of parameters when {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher dispatcher}
  732. * fires its events.
  733. *
  734. * @interface module:engine/conversion/downcastdispatcher~DowncastConversionApi
  735. */
  736. /**
  737. * The {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} instance.
  738. *
  739. * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #dispatcher
  740. */
  741. /**
  742. * Stores information about what parts of processed model item are still waiting to be handled. After a piece of model item
  743. * was converted, appropriate consumable value should be {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}.
  744. *
  745. * @member {module:engine/conversion/modelconsumable~ModelConsumable} #consumable
  746. */
  747. /**
  748. * The {@link module:engine/conversion/mapper~Mapper} instance.
  749. *
  750. * @member {module:engine/conversion/mapper~Mapper} #mapper
  751. */
  752. /**
  753. * The {@link module:engine/model/schema~Schema} instance set for the model that is downcast.
  754. *
  755. * @member {module:engine/model/schema~Schema} #schema
  756. */
  757. /**
  758. * The {@link module:engine/view/downcastwriter~DowncastWriter} instance used to manipulate data during conversion.
  759. *
  760. * @member {module:engine/view/downcastwriter~DowncastWriter} #writer
  761. */
  762. /**
  763. * An object with an additional configuration which can be used during conversion process. Available only for data downcast conversion.
  764. *
  765. * @member {Object} #options
  766. */