modelconversiondispatcher.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/conversion/modelconversiondispatcher
  7. */
  8. import Consumable from './modelconsumable';
  9. import Range from '../model/range';
  10. import Position from '../model/position';
  11. import DocumentFragment from '../model/documentfragment';
  12. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  13. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  14. import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
  15. /**
  16. * `ModelConversionDispatcher` is a central point of {@link module:engine/model/model model} conversion, which is
  17. * a process of reacting to changes in the model and reflecting them by listeners that listen to those changes.
  18. * In default application, {@link module:engine/model/model model} is converted to {@link module:engine/view/view view}. This means
  19. * that changes in the model are reflected by changing the view (i.e. adding view nodes or changing attributes on view elements).
  20. *
  21. * During conversion process, `ModelConversionDispatcher` fires data-manipulation events, basing on state of the model and prepares
  22. * data for those events. It is important to note that the events are connected with "change actions" like "inserting"
  23. * or "removing" so one might say that we are converting "changes". This is in contrary to view to model conversion,
  24. * where we convert view nodes (the structure, not "changes" to the view). Note, that because changes are converted
  25. * and not the structure itself, there is a need to have a mapping between model and the structure on which changes are
  26. * reflected. To map elements during model to view conversion use {@link module:engine/conversion/mapper~Mapper}.
  27. *
  28. * The main use for this class is to listen to {@link module:engine/model/document~Document#event:change Document change event}, process it
  29. * and then fire specific events telling what exactly has changed. For those events, `ModelConversionDispatcher`
  30. * creates {@link module:engine/conversion/modelconsumable~ModelConsumable list of consumable values} that should be handled by event
  31. * callbacks. Those events are listened to by model-to-view converters which convert changes done in the
  32. * {@link module:engine/model/model model} to changes in the {@link module:engine/view/view view}. `ModelConversionController` also checks
  33. * the current state of consumables, so it won't fire events for parts of model that were already consumed. This is
  34. * especially important in callbacks that consume multiple values. See {@link module:engine/conversion/modelconsumable~ModelConsumable}
  35. * for an example of such callback.
  36. *
  37. * Although the primary usage for this class is the model-to-view conversion, `ModelConversionDispatcher` can be used
  38. * to build custom data processing pipelines that converts model to anything that is needed. Existing model structure can
  39. * be used to generate events (listening to {@link module:engine/model/document~Document#event:change Document change event} is not
  40. * required)
  41. * and custom callbacks can be added to the events (these does not have to be limited to changes in the view).
  42. *
  43. * When providing your own event listeners for `ModelConversionDispatcher` keep in mind that any callback that had
  44. * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from consumable (and did some changes, i.e. to
  45. * the view) should also stop the event. This is because whenever a callback is fired it is assumed that there is something
  46. * to be consumed. Thanks to that approach, you do not have to test whether there is anything to consume at the beginning
  47. * of your listener callback.
  48. *
  49. * Example of providing a converter for `ModelConversionDispatcher`:
  50. *
  51. * // We will convert inserting "paragraph" model element into the model.
  52. * modelDispatcher.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => {
  53. * // Remember to consume the part of consumable.
  54. * consumable.consume( data.item, 'insert' );
  55. *
  56. * // Translate position in model to position in the view.
  57. * const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  58. *
  59. * // Create a P element (note that this converter is for inserting P elements -> 'insert:paragraph').
  60. * const viewElement = new ViewElement( 'p' );
  61. *
  62. * // Bind the newly created view element to model element so positions will map accordingly in future.
  63. * conversionApi.mapper.bindElements( data.item, viewElement );
  64. *
  65. * // Add the newly created view element to the view.
  66. * viewWriter.insert( viewPosition, viewElement );
  67. *
  68. * // Remember to stop the event propagation if the data.item was consumed.
  69. * evt.stop();
  70. * } );
  71. *
  72. * Callback that "overrides" other callback:
  73. *
  74. * // Special converter for `linkHref` attribute added on custom `quote` element. Note, that this
  75. * // attribute may be the same as the attribute added by other features (link feature in this case).
  76. * // It might be even added by that feature! It makes sense that a part of content that is a quote is linked
  77. * // to an external source so it makes sense that link feature works on the custom quote element.
  78. * // However, we have to make sure that the attributes added by link feature are correctly converted.
  79. * // To block default `linkHref` conversion we have to:
  80. * // 1) add this callback with higher priority than link feature callback,
  81. * // 2) consume `linkHref` attribute add change.
  82. * modelConversionDispatcher.on( 'addAttribute:linkHref:quote', ( evt, data, consumable, conversionApi ) => {
  83. * consumable.consume( data.item, 'addAttribute:linkHref' );
  84. *
  85. * // Create a button that will represent the `linkHref` attribute.
  86. * let viewSourceBtn = new ViewElement( 'a', {
  87. * href: data.item.getAttribute( 'linkHref' ),
  88. * title: 'source'
  89. * } );
  90. *
  91. * // Add a class for the button.
  92. * viewSourceBtn.addClass( 'source' );
  93. *
  94. * // Insert the button using writer API.
  95. * // If `addAttribute` event is fired by
  96. * // `module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#convertInsert` it is fired
  97. * // after `data.item` insert conversion was done. If the event is fired due to attribute insertion coming from
  98. * // different source, `data.item` already existed. This means we are safe to get `viewQuote` from mapper.
  99. * const viewQuote = conversionApi.mapper.toViewElement( data.item );
  100. * const position = new ViewPosition( viewQuote, viewQuote.childCount );
  101. * viewWriter.insert( position, viewSourceBtn );
  102. *
  103. * evt.stop();
  104. * }, { priority: 'high' } );
  105. */
  106. export default class ModelConversionDispatcher {
  107. /**
  108. * Creates a `ModelConversionDispatcher` that operates using passed API.
  109. *
  110. * @param {module:engine/model/model~Model} model Data model.
  111. * @param {Object} [conversionApi] Interface passed by dispatcher to the events callbacks.
  112. */
  113. constructor( model, conversionApi = {} ) {
  114. /**
  115. * Data model instance bound with this dispatcher.
  116. *
  117. * @private
  118. * @member {module:engine/model/model~Model}
  119. */
  120. this._model = model;
  121. /**
  122. * Interface passed by dispatcher to the events callbacks.
  123. *
  124. * @member {Object}
  125. */
  126. this.conversionApi = extend( { dispatcher: this }, conversionApi );
  127. }
  128. /**
  129. * Prepares data and fires a proper event.
  130. *
  131. * The method is crafted to take use of parameters passed in {@link module:engine/model/document~Document#event:change Document change
  132. * event}.
  133. *
  134. * @see module:engine/model/document~Document#event:change
  135. * @fires insert
  136. * @fires remove
  137. * @fires addAttribute
  138. * @fires removeAttribute
  139. * @fires changeAttribute
  140. * @fires addMarker
  141. * @param {String} type Change type.
  142. * @param {Object} data Additional information about the change.
  143. */
  144. convertChange( type, data ) {
  145. // Do not convert changes if they happen in graveyard.
  146. // Graveyard is a special root that has no view / no other representation and changes done in it should not be converted.
  147. if ( type !== 'remove' && data.range && data.range.root.rootName == '$graveyard' ) {
  148. return;
  149. }
  150. if ( type == 'remove' && data.sourcePosition.root.rootName == '$graveyard' ) {
  151. return;
  152. }
  153. if ( type == 'rename' && data.element.root.rootName == '$graveyard' ) {
  154. return;
  155. }
  156. // We can safely dispatch changes.
  157. if ( type == 'insert' || type == 'reinsert' ) {
  158. this.convertInsertion( data.range );
  159. } else if ( type == 'move' ) {
  160. this.convertMove( data.sourcePosition, data.range );
  161. } else if ( type == 'remove' ) {
  162. this.convertRemove( data.sourcePosition, data.range );
  163. } else if ( type == 'addAttribute' || type == 'removeAttribute' || type == 'changeAttribute' ) {
  164. this.convertAttribute( type, data.range, data.key, data.oldValue, data.newValue );
  165. } else if ( type == 'rename' ) {
  166. this.convertRename( data.element, data.oldName );
  167. }
  168. }
  169. /**
  170. * Starts conversion of insertion-change on given `range`.
  171. *
  172. * Analyzes given range and fires insertion-connected events with data based on that range.
  173. *
  174. * **Note**: This method will fire separate events for node insertion and attributes insertion. All
  175. * attributes that are set on inserted nodes are treated like they were added just after node insertion.
  176. *
  177. * @fires insert
  178. * @fires addAttribute
  179. * @fires addMarker
  180. * @param {module:engine/model/range~Range} range Inserted range.
  181. */
  182. convertInsertion( range ) {
  183. // Create a list of things that can be consumed, consisting of nodes and their attributes.
  184. const consumable = this._createInsertConsumable( range );
  185. // Fire a separate insert event for each node and text fragment contained in the range.
  186. for ( const value of range ) {
  187. const item = value.item;
  188. const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
  189. const data = {
  190. item,
  191. range: itemRange
  192. };
  193. this._testAndFire( 'insert', data, consumable );
  194. // Fire a separate addAttribute event for each attribute that was set on inserted items.
  195. // This is important because most attributes converters will listen only to add/change/removeAttribute events.
  196. // If we would not add this part, attributes on inserted nodes would not be converted.
  197. for ( const key of item.getAttributeKeys() ) {
  198. data.attributeKey = key;
  199. data.attributeOldValue = null;
  200. data.attributeNewValue = item.getAttribute( key );
  201. this._testAndFire( `addAttribute:${ key }`, data, consumable );
  202. }
  203. }
  204. for ( const marker of this._model.markers ) {
  205. const markerRange = marker.getRange();
  206. const intersection = markerRange.getIntersection( range );
  207. // Check if inserted content is inserted into a marker.
  208. if ( intersection && shouldMarkerChangeBeConverted( range.start, marker, this.conversionApi.mapper ) ) {
  209. this.convertMarker( 'addMarker', marker.name, intersection );
  210. }
  211. }
  212. }
  213. /**
  214. * Starts conversion of move-change of given `range`, that was moved from given `sourcePosition`.
  215. *
  216. * Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed parameters.
  217. *
  218. * @fires remove
  219. * @fires insert
  220. * @param {module:engine/model/position~Position} sourcePosition The original position from which the range was moved.
  221. * @param {module:engine/model/range~Range} range The range containing the moved content.
  222. */
  223. convertMove( sourcePosition, range ) {
  224. // Move left – convert insertion first (#847).
  225. if ( range.start.isBefore( sourcePosition ) ) {
  226. this.convertInsertion( range );
  227. const sourcePositionAfterInsertion =
  228. sourcePosition._getTransformedByInsertion( range.start, range.end.offset - range.start.offset );
  229. this.convertRemove( sourcePositionAfterInsertion, range );
  230. } else {
  231. this.convertRemove( sourcePosition, range );
  232. this.convertInsertion( range );
  233. }
  234. }
  235. /**
  236. * Starts conversion of remove-change of given `range`, that was removed from given `sourcePosition`.
  237. *
  238. * Fires {@link ~#event:remove remove event} with data based on passed values.
  239. *
  240. * @fires remove
  241. * @param {module:engine/model/position~Position} sourcePosition Position from where the range has been removed.
  242. * @param {module:engine/model/range~Range} range Removed range (after remove, in
  243. * {@link module:engine/model/document~Document#graveyard graveyard root}).
  244. */
  245. convertRemove( sourcePosition, range ) {
  246. const consumable = this._createConsumableForRange( range, 'remove' );
  247. for ( const item of range.getItems( { shallow: true } ) ) {
  248. const data = {
  249. sourcePosition,
  250. item
  251. };
  252. this._testAndFire( 'remove', data, consumable );
  253. }
  254. }
  255. /**
  256. * Starts conversion of attribute-change on given `range`.
  257. *
  258. * Analyzes given attribute change and fires attributes-connected events with data based on passed values.
  259. *
  260. * @fires addAttribute
  261. * @fires removeAttribute
  262. * @fires changeAttribute
  263. * @param {String} type Change type. Possible values: `addAttribute`, `removeAttribute`, `changeAttribute`.
  264. * @param {module:engine/model/range~Range} range Changed range.
  265. * @param {String} key Attribute key.
  266. * @param {*} oldValue Attribute value before the change or `null` if attribute has not been set.
  267. * @param {*} newValue New attribute value or `null` if attribute has been removed.
  268. */
  269. convertAttribute( type, range, key, oldValue, newValue ) {
  270. if ( oldValue == newValue ) {
  271. // Do not convert if the attribute did not change.
  272. return;
  273. }
  274. // Create a list with attributes to consume.
  275. const consumable = this._createConsumableForRange( range, type + ':' + key );
  276. // Create a separate attribute event for each node in the range.
  277. for ( const value of range ) {
  278. const item = value.item;
  279. const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length );
  280. const data = {
  281. item,
  282. range: itemRange,
  283. attributeKey: key,
  284. attributeOldValue: oldValue,
  285. attributeNewValue: newValue
  286. };
  287. this._testAndFire( `${ type }:${ key }`, data, consumable );
  288. }
  289. }
  290. /**
  291. * Starts conversion of rename-change of given `element` that had given `oldName`.
  292. *
  293. * Fires {@link ~#event:remove remove event} and {@link ~#event:insert insert event} based on passed values.
  294. *
  295. * @fires remove
  296. * @fires insert
  297. * @param {module:engine/model/element~Element} element Renamed element.
  298. * @param {String} oldName Name of the renamed element before it was renamed.
  299. */
  300. convertRename( element, oldName ) {
  301. if ( element.name == oldName ) {
  302. // Do not convert if the name did not change.
  303. return;
  304. }
  305. // Create fake element that will be used to fire remove event. The fake element will have the old element name.
  306. const fakeElement = element.clone( true );
  307. fakeElement.name = oldName;
  308. // Bind fake element with original view element so the view element will be removed.
  309. this.conversionApi.mapper.bindElements(
  310. fakeElement,
  311. this.conversionApi.mapper.toViewElement( element )
  312. );
  313. // Create fake document fragment so a range can be created on fake element.
  314. const fakeDocumentFragment = new DocumentFragment();
  315. fakeDocumentFragment.appendChildren( fakeElement );
  316. this.convertRemove( Position.createBefore( element ), Range.createOn( fakeElement ) );
  317. this.convertInsertion( Range.createOn( element ) );
  318. }
  319. /**
  320. * Starts selection conversion.
  321. *
  322. * Fires events for given {@link module:engine/model/selection~Selection selection} to start selection conversion.
  323. *
  324. * @fires selection
  325. * @fires selectionAttribute
  326. * @param {module:engine/model/selection~Selection} selection Selection to convert.
  327. */
  328. convertSelection( selection ) {
  329. const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) );
  330. const consumable = this._createSelectionConsumable( selection, markers );
  331. this.fire( 'selection', { selection }, consumable, this.conversionApi );
  332. for ( const marker of markers ) {
  333. const markerRange = marker.getRange();
  334. if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) {
  335. continue;
  336. }
  337. const data = {
  338. selection,
  339. markerName: marker.name,
  340. markerRange
  341. };
  342. if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) {
  343. this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi );
  344. }
  345. }
  346. for ( const key of selection.getAttributeKeys() ) {
  347. const data = {
  348. selection,
  349. key,
  350. value: selection.getAttribute( key )
  351. };
  352. // Do not fire event if the attribute has been consumed.
  353. if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) {
  354. this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi );
  355. }
  356. }
  357. }
  358. /**
  359. * Starts marker conversion.
  360. *
  361. * Fires {@link ~#event:addMarker addMarker} or {@link ~#event:removeMarker removeMarker} events for each item
  362. * in marker's range. If range is collapsed single event is dispatched. See events description for more details.
  363. *
  364. * @fires addMarker
  365. * @fires removeMarker
  366. * @param {'addMarker'|'removeMarker'} type Change type.
  367. * @param {String} name Marker name.
  368. * @param {module:engine/model/range~Range} range Marker range.
  369. */
  370. convertMarker( type, name, range ) {
  371. // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
  372. if ( !range.root.document || range.root.rootName == '$graveyard' ) {
  373. return;
  374. }
  375. // In markers case, event name == consumable name.
  376. const eventName = type + ':' + name;
  377. // When range is collapsed - fire single event with collapsed range in consumable.
  378. if ( range.isCollapsed ) {
  379. const consumable = new Consumable();
  380. consumable.add( range, eventName );
  381. this.fire( eventName, {
  382. markerName: name,
  383. markerRange: range
  384. }, consumable, this.conversionApi );
  385. return;
  386. }
  387. // Create consumable for each item in range.
  388. const consumable = this._createConsumableForRange( range, eventName );
  389. // Create separate event for each node in the range.
  390. for ( const value of range ) {
  391. const item = value.item;
  392. // Do not fire event for already consumed items.
  393. if ( !consumable.test( item, eventName ) ) {
  394. continue;
  395. }
  396. const data = {
  397. item,
  398. range: Range.createFromPositionAndShift( value.previousPosition, value.length ),
  399. markerName: name,
  400. markerRange: range
  401. };
  402. this.fire( eventName, data, consumable, this.conversionApi );
  403. }
  404. }
  405. /**
  406. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values to consume from given range, assuming that
  407. * given range has just been inserted to the model.
  408. *
  409. * @private
  410. * @param {module:engine/model/range~Range} range Inserted range.
  411. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  412. */
  413. _createInsertConsumable( range ) {
  414. const consumable = new Consumable();
  415. for ( const value of range ) {
  416. const item = value.item;
  417. consumable.add( item, 'insert' );
  418. for ( const key of item.getAttributeKeys() ) {
  419. consumable.add( item, 'addAttribute:' + key );
  420. }
  421. }
  422. return consumable;
  423. }
  424. /**
  425. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with values of given `type`
  426. * for each item from given `range`.
  427. *
  428. * @private
  429. * @param {module:engine/model/range~Range} range Affected range.
  430. * @param {String} type Consumable type.
  431. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  432. */
  433. _createConsumableForRange( range, type ) {
  434. const consumable = new Consumable();
  435. for ( const item of range.getItems() ) {
  436. consumable.add( item, type );
  437. }
  438. return consumable;
  439. }
  440. /**
  441. * Creates {@link module:engine/conversion/modelconsumable~ModelConsumable} with selection consumable values.
  442. *
  443. * @private
  444. * @param {module:engine/model/selection~Selection} selection Selection to create consumable from.
  445. * @param {Iterable.<module:engine/model/markercollection~Marker>} markers Markers which contains selection.
  446. * @returns {module:engine/conversion/modelconsumable~ModelConsumable} Values to consume.
  447. */
  448. _createSelectionConsumable( selection, markers ) {
  449. const consumable = new Consumable();
  450. consumable.add( selection, 'selection' );
  451. for ( const marker of markers ) {
  452. consumable.add( selection, 'selectionMarker:' + marker.name );
  453. }
  454. for ( const key of selection.getAttributeKeys() ) {
  455. consumable.add( selection, 'selectionAttribute:' + key );
  456. }
  457. return consumable;
  458. }
  459. /**
  460. * Tests passed `consumable` to check whether given event can be fired and if so, fires it.
  461. *
  462. * @private
  463. * @fires insert
  464. * @fires remove
  465. * @fires addAttribute
  466. * @fires removeAttribute
  467. * @fires changeAttribute
  468. * @param {String} type Event type.
  469. * @param {Object} data Event data.
  470. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  471. */
  472. _testAndFire( type, data, consumable ) {
  473. if ( !consumable.test( data.item, type ) ) {
  474. // Do not fire event if the item was consumed.
  475. return;
  476. }
  477. const name = data.item.name || '$text';
  478. this.fire( type + ':' + name, data, consumable, this.conversionApi );
  479. }
  480. /**
  481. * Fired for inserted nodes.
  482. *
  483. * `insert` is a namespace for a class of events. Names of actually called events follow this pattern:
  484. * `insert:<name>`. `name` is either `'$text'` when one or more characters has been inserted or
  485. * {@link module:engine/model/element~Element#name name} of inserted element.
  486. *
  487. * This way listeners can either listen to a general `insert` event or specific event (for example `insert:paragraph`).
  488. *
  489. * @event insert
  490. * @param {Object} data Additional information about the change.
  491. * @param {module:engine/model/item~Item} data.item Inserted item.
  492. * @param {module:engine/model/range~Range} data.range Range spanning over inserted item.
  493. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  494. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  495. */
  496. /**
  497. * Fired for removed nodes.
  498. *
  499. * `remove` is a namespace for a class of events. Names of actually called events follow this pattern:
  500. * `remove:<name>`. `name` is either `'$text'` when one or more characters has been removed or the
  501. * {@link module:engine/model/element~Element#name name} of removed element.
  502. *
  503. * This way listeners can either listen to a general `remove` event or specific event (for example `remove:paragraph`).
  504. *
  505. * @event remove
  506. * @param {Object} data Additional information about the change.
  507. * @param {module:engine/model/position~Position} data.sourcePosition Position from where the range has been removed.
  508. * @param {module:engine/model/range~Range} data.range Removed range (in {@link module:engine/model/document~Document#graveyard
  509. * graveyard root}).
  510. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  511. */
  512. /**
  513. * Fired when attribute has been added on a node.
  514. *
  515. * `addAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  516. * `addAttribute:<attributeKey>:<name>`. `attributeKey` is the key of added attribute. `name` is either `'$text'`
  517. * if attribute was added on one or more characters, or the {@link module:engine/model/element~Element#name name} of
  518. * the element on which attribute was added.
  519. *
  520. * This way listeners can either listen to a general `addAttribute:bold` event or specific event
  521. * (for example `addAttribute:link:image`).
  522. *
  523. * @event addAttribute
  524. * @param {Object} data Additional information about the change.
  525. * @param {module:engine/model/item~Item} data.item Changed item.
  526. * @param {module:engine/model/range~Range} data.range Range spanning over changed item.
  527. * @param {String} data.attributeKey Attribute key.
  528. * @param {null} data.attributeOldValue Attribute value before the change - always `null`. Kept for the sake of unifying events.
  529. * @param {*} data.attributeNewValue New attribute value.
  530. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  531. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  532. */
  533. /**
  534. * Fired when attribute has been removed from a node.
  535. *
  536. * `removeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  537. * `removeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of removed attribute. `name` is either `'$text'`
  538. * if attribute was removed from one or more characters, or the {@link module:engine/model/element~Element#name name} of
  539. * the element from which attribute was removed.
  540. *
  541. * This way listeners can either listen to a general `removeAttribute:bold` event or specific event
  542. * (for example `removeAttribute:link:image`).
  543. *
  544. * @event removeAttribute
  545. * @param {Object} data Additional information about the change.
  546. * @param {module:engine/model/item~Item} data.item Changed item.
  547. * @param {module:engine/model/range~Range} data.range Range spanning over changed item.
  548. * @param {String} data.attributeKey Attribute key.
  549. * @param {*} data.attributeOldValue Attribute value before it was removed.
  550. * @param {null} data.attributeNewValue New attribute value - always `null`. Kept for the sake of unifying events.
  551. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  552. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  553. */
  554. /**
  555. * Fired when attribute of a node has been changed.
  556. *
  557. * `changeAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  558. * `changeAttribute:<attributeKey>:<name>`. `attributeKey` is the key of changed attribute. `name` is either `'$text'`
  559. * if attribute was changed on one or more characters, or the {@link module:engine/model/element~Element#name name} of
  560. * the element on which attribute was changed.
  561. *
  562. * This way listeners can either listen to a general `changeAttribute:link` event or specific event
  563. * (for example `changeAttribute:link:image`).
  564. *
  565. * @event changeAttribute
  566. * @param {Object} data Additional information about the change.
  567. * @param {module:engine/model/item~Item} data.item Changed item.
  568. * @param {module:engine/model/range~Range} data.range Range spanning over changed item.
  569. * @param {String} data.attributeKey Attribute key.
  570. * @param {*} data.attributeOldValue Attribute value before the change.
  571. * @param {*} data.attributeNewValue New attribute value.
  572. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  573. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  574. */
  575. /**
  576. * Fired for {@link module:engine/model/selection~Selection selection} changes.
  577. *
  578. * @event selection
  579. * @param {module:engine/model/selection~Selection} selection `Selection` instance that is converted.
  580. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  581. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  582. */
  583. /**
  584. * Fired for {@link module:engine/model/selection~Selection selection} attributes changes.
  585. *
  586. * `selectionAttribute` is a namespace for a class of events. Names of actually called events follow this pattern:
  587. * `selectionAttribute:<attributeKey>`. `attributeKey` is the key of selection attribute. This way listen can listen to
  588. * certain attribute, i.e. `addAttribute:bold`.
  589. *
  590. * @event selectionAttribute
  591. * @param {Object} data Additional information about the change.
  592. * @param {module:engine/model/selection~Selection} data.selection Selection that is converted.
  593. * @param {String} data.attributeKey Key of changed attribute.
  594. * @param {*} data.attributeValue Value of changed attribute.
  595. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  596. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  597. */
  598. /**
  599. * Fired when a new marker is added to the model.
  600. * * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this
  601. * situation, consumable contains all items from that range.
  602. * * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range.
  603. *
  604. * `addMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  605. * `addMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
  606. * markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `addMarker:foo` or `addMarker:foo:abc` and
  607. * `addMarker:foo:bar` events.
  608. *
  609. * @event addMarker
  610. * @param {Object} data Additional information about the change.
  611. * @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range
  612. * is being converted.
  613. * @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range
  614. * is being converted.
  615. * @param {String} data.markerName Name of the marker.
  616. * @param {module:engine/model/range~Range} data.markerRange Marker's range spanning on all items.
  617. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed
  618. * marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains
  619. * only marker's range to consume.
  620. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  621. */
  622. /**
  623. * Fired when marker is removed from the model.
  624. * * If marker's range is not collapsed, event is fired separately for each item contained in that range. In this
  625. * situation, consumable contains all items from that range.
  626. * * If marker's range is collapsed, single event is fired. In this situation, consumable contains only the collapsed range.
  627. *
  628. * `removeMarker` is a namespace for a class of events. Names of actually called events follow this pattern:
  629. * `removeMarker:<markerName>`. By specifying certain marker names, you can make the events even more gradual. For example,
  630. * markers can be named `foo:abc`, `foo:bar`, then it is possible to listen to `removeMarker:foo` or `removeMarker:foo:abc` and
  631. * `removeMarker:foo:bar` events.
  632. *
  633. * @event removeMarker
  634. * @param {Object} data Additional information about the change.
  635. * @param {module:engine/model/item~Item} [data.item] Item contained in marker's range. Not present if collapsed range
  636. * is being converted.
  637. * @param {module:engine/model/range~Range} [data.range] Range spanning over item. Not present if collapsed range
  638. * is being converted.
  639. * @param {String} data.markerName Name of the marker.
  640. * @param {module:engine/model/range~Range} data.markerRange Whole marker's range.
  641. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume. When non-collapsed
  642. * marker is being converted then consumable contains all items in marker's range. For collapsed markers it contains
  643. * only marker's range to consume.
  644. * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ModelConversionDispatcher` constructor.
  645. */
  646. }
  647. mix( ModelConversionDispatcher, EmitterMixin );
  648. // Helper function, checks whether change of `marker` at `modelPosition` should be converted. Marker changes are not
  649. // converted if they happen inside an element with custom conversion method.
  650. //
  651. // @param {module:engine/model/position~Position} modelPosition
  652. // @param {module:engine/model/markercollection~Marker} marker
  653. // @param {module:engine/conversion/mapper~Mapper} mapper
  654. // @returns {Boolean}
  655. function shouldMarkerChangeBeConverted( modelPosition, marker, mapper ) {
  656. const range = marker.getRange();
  657. const ancestors = Array.from( modelPosition.getAncestors() );
  658. ancestors.shift(); // Remove root element. It cannot be passed to `model.Range#containsItem`.
  659. ancestors.reverse();
  660. const hasCustomHandling = ancestors.some( element => {
  661. if ( range.containsItem( element ) ) {
  662. const viewElement = mapper.toViewElement( element );
  663. return !!viewElement.getCustomProperty( 'addHighlight' );
  664. }
  665. } );
  666. return !hasCustomHandling;
  667. }