mapper.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. /**
  2. * @license Copyright (c) 2003-2019, 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/mapper
  7. */
  8. import ModelPosition from '../model/position';
  9. import ModelRange from '../model/range';
  10. import ViewPosition from '../view/position';
  11. import ViewRange from '../view/range';
  12. import ViewText from '../view/text';
  13. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  14. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  15. /**
  16. * Maps elements, positions and markers between {@link module:engine/view/document~Document the view} and
  17. * {@link module:engine/model/model the model}.
  18. *
  19. * The instance of the Mapper used for the editing pipeline is available in
  20. * {@link module:engine/controller/editingcontroller~EditingController#mapper `editor.editing.mapper`}.
  21. *
  22. * Mapper uses bound elements to find corresponding elements and positions, so, to get proper results,
  23. * all model elements should be {@link module:engine/conversion/mapper~Mapper#bindElements bound}.
  24. *
  25. * To map complex model to/from view relations, you may provide custom callbacks for
  26. * {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition modelToViewPosition event} and
  27. * {@link module:engine/conversion/mapper~Mapper#event:viewToModelPosition viewToModelPosition event} that are fired whenever
  28. * a position mapping request occurs.
  29. * Those events are fired by {@link module:engine/conversion/mapper~Mapper#toViewPosition toViewPosition}
  30. * and {@link module:engine/conversion/mapper~Mapper#toModelPosition toModelPosition} methods. `Mapper` adds it's own default callbacks
  31. * with `'lowest'` priority. To override default `Mapper` mapping, add custom callback with higher priority and
  32. * stop the event.
  33. */
  34. export default class Mapper {
  35. /**
  36. * Creates an instance of the mapper.
  37. */
  38. constructor() {
  39. /**
  40. * Model element to view element mapping.
  41. *
  42. * @private
  43. * @member {WeakMap}
  44. */
  45. this._modelToViewMapping = new WeakMap();
  46. /**
  47. * View element to model element mapping.
  48. *
  49. * @private
  50. * @member {WeakMap}
  51. */
  52. this._viewToModelMapping = new WeakMap();
  53. /**
  54. * A map containing callbacks between view element names and functions evaluating length of view elements
  55. * in model.
  56. *
  57. * @private
  58. * @member {Map}
  59. */
  60. this._viewToModelLengthCallbacks = new Map();
  61. /**
  62. * Model marker name to view elements mapping.
  63. *
  64. * Keys are `String`s while values are `Set`s with {@link module:engine/view/element~Element view elements}.
  65. * One marker (name) can be mapped to multiple elements.
  66. *
  67. * @private
  68. * @member {Map}
  69. */
  70. this._markerNameToElements = new Map();
  71. /**
  72. * View element to model marker names mapping.
  73. *
  74. * This is reverse to {@link ~Mapper#_markerNameToElements} map.
  75. *
  76. * @private
  77. * @member {Map}
  78. */
  79. this._elementToMarkerNames = new Map();
  80. /**
  81. * Stores marker names of markers which has changed due to unbinding a view element (so it is assumed that the view element
  82. * has been removed, moved or renamed).
  83. *
  84. * @private
  85. * @member {Set.<module:engine/model/markercollection~Marker>}
  86. */
  87. this._unboundMarkerNames = new Set();
  88. // Default mapper algorithm for mapping model position to view position.
  89. this.on( 'modelToViewPosition', ( evt, data ) => {
  90. if ( data.viewPosition ) {
  91. return;
  92. }
  93. const viewContainer = this._modelToViewMapping.get( data.modelPosition.parent );
  94. data.viewPosition = this._findPositionIn( viewContainer, data.modelPosition.offset );
  95. }, { priority: 'low' } );
  96. // Default mapper algorithm for mapping view position to model position.
  97. this.on( 'viewToModelPosition', ( evt, data ) => {
  98. if ( data.modelPosition ) {
  99. return;
  100. }
  101. const viewBlock = this.findMappedViewAncestor( data.viewPosition );
  102. const modelParent = this._viewToModelMapping.get( viewBlock );
  103. const modelOffset = this._toModelOffset( data.viewPosition.parent, data.viewPosition.offset, viewBlock );
  104. data.modelPosition = ModelPosition._createAt( modelParent, modelOffset );
  105. }, { priority: 'low' } );
  106. }
  107. /**
  108. * Marks model and view elements as corresponding. Corresponding elements can be retrieved by using
  109. * the {@link module:engine/conversion/mapper~Mapper#toModelElement toModelElement} and
  110. * {@link module:engine/conversion/mapper~Mapper#toViewElement toViewElement} methods.
  111. * The information that elements are bound is also used to translate positions.
  112. *
  113. * @param {module:engine/model/element~Element} modelElement Model element.
  114. * @param {module:engine/view/element~Element} viewElement View element.
  115. */
  116. bindElements( modelElement, viewElement ) {
  117. this._modelToViewMapping.set( modelElement, viewElement );
  118. this._viewToModelMapping.set( viewElement, modelElement );
  119. }
  120. /**
  121. * Unbinds given {@link module:engine/view/element~Element view element} from the map.
  122. *
  123. * **Note:** view-to-model binding will be removed, if it existed. However, corresponding model-to-view binding
  124. * will be removed only if model element is still bound to passed `viewElement`.
  125. *
  126. * This behavior lets for re-binding model element to another view element without fear of losing the new binding
  127. * when the previously bound view element is unbound.
  128. *
  129. * @param {module:engine/view/element~Element} viewElement View element to unbind.
  130. */
  131. unbindViewElement( viewElement ) {
  132. const modelElement = this.toModelElement( viewElement );
  133. this._viewToModelMapping.delete( viewElement );
  134. if ( this._elementToMarkerNames.has( viewElement ) ) {
  135. for ( const markerName of this._elementToMarkerNames.get( viewElement ) ) {
  136. this._unboundMarkerNames.add( markerName );
  137. }
  138. }
  139. if ( this._modelToViewMapping.get( modelElement ) == viewElement ) {
  140. this._modelToViewMapping.delete( modelElement );
  141. }
  142. }
  143. /**
  144. * Unbinds given {@link module:engine/model/element~Element model element} from the map.
  145. *
  146. * **Note:** model-to-view binding will be removed, if it existed. However, corresponding view-to-model binding
  147. * will be removed only if view element is still bound to passed `modelElement`.
  148. *
  149. * This behavior lets for re-binding view element to another model element without fear of losing the new binding
  150. * when the previously bound model element is unbound.
  151. *
  152. * @param {module:engine/model/element~Element} modelElement Model element to unbind.
  153. */
  154. unbindModelElement( modelElement ) {
  155. const viewElement = this.toViewElement( modelElement );
  156. this._modelToViewMapping.delete( modelElement );
  157. if ( this._viewToModelMapping.get( viewElement ) == modelElement ) {
  158. this._viewToModelMapping.delete( viewElement );
  159. }
  160. }
  161. /**
  162. * Binds given marker name with given {@link module:engine/view/element~Element view element}. The element
  163. * will be added to the current set of elements bound with given marker name.
  164. *
  165. * @param {module:engine/view/element~Element} element Element to bind.
  166. * @param {String} name Marker name.
  167. */
  168. bindElementToMarker( element, name ) {
  169. const elements = this._markerNameToElements.get( name ) || new Set();
  170. elements.add( element );
  171. const names = this._elementToMarkerNames.get( element ) || new Set();
  172. names.add( name );
  173. this._markerNameToElements.set( name, elements );
  174. this._elementToMarkerNames.set( element, names );
  175. }
  176. /**
  177. * Unbinds an element from given marker name.
  178. *
  179. * @param {module:engine/view/element~Element} element Element to unbind.
  180. * @param {String} name Marker name.
  181. */
  182. unbindElementFromMarkerName( element, name ) {
  183. const nameToElements = this._markerNameToElements.get( name );
  184. if ( nameToElements ) {
  185. nameToElements.delete( element );
  186. if ( nameToElements.size == 0 ) {
  187. this._markerNameToElements.delete( name );
  188. }
  189. }
  190. const elementToNames = this._elementToMarkerNames.get( element );
  191. if ( elementToNames ) {
  192. elementToNames.delete( name );
  193. if ( elementToNames.size == 0 ) {
  194. this._elementToMarkerNames.delete( element );
  195. }
  196. }
  197. }
  198. /**
  199. * Returns all marker names of markers which has changed due to unbinding a view element (so it is assumed that the view element
  200. * has been removed, moved or renamed) since the last flush. After returning, the marker names list is cleared.
  201. *
  202. * @returns {Array.<String>}
  203. */
  204. flushUnboundMarkerNames() {
  205. const markerNames = Array.from( this._unboundMarkerNames );
  206. this._unboundMarkerNames.clear();
  207. return markerNames;
  208. }
  209. /**
  210. * Removes all model to view and view to model bindings.
  211. */
  212. clearBindings() {
  213. this._modelToViewMapping = new WeakMap();
  214. this._viewToModelMapping = new WeakMap();
  215. this._markerNameToElements = new Map();
  216. this._elementToMarkerNames = new Map();
  217. this._unboundMarkerNames = new Set();
  218. }
  219. /**
  220. * Gets the corresponding model element.
  221. *
  222. * **Note:** {@link module:engine/view/uielement~UIElement} does not have corresponding element in model.
  223. *
  224. * @param {module:engine/view/element~Element} viewElement View element.
  225. * @returns {module:engine/model/element~Element|undefined} Corresponding model element or `undefined` if not found.
  226. */
  227. toModelElement( viewElement ) {
  228. return this._viewToModelMapping.get( viewElement );
  229. }
  230. /**
  231. * Gets the corresponding view element.
  232. *
  233. * @param {module:engine/model/element~Element} modelElement Model element.
  234. * @returns {module:engine/view/element~Element|undefined} Corresponding view element or `undefined` if not found.
  235. */
  236. toViewElement( modelElement ) {
  237. return this._modelToViewMapping.get( modelElement );
  238. }
  239. /**
  240. * Gets the corresponding model range.
  241. *
  242. * @param {module:engine/view/range~Range} viewRange View range.
  243. * @returns {module:engine/model/range~Range} Corresponding model range.
  244. */
  245. toModelRange( viewRange ) {
  246. return new ModelRange( this.toModelPosition( viewRange.start ), this.toModelPosition( viewRange.end ) );
  247. }
  248. /**
  249. * Gets the corresponding view range.
  250. *
  251. * @param {module:engine/model/range~Range} modelRange Model range.
  252. * @returns {module:engine/view/range~Range} Corresponding view range.
  253. */
  254. toViewRange( modelRange ) {
  255. return new ViewRange( this.toViewPosition( modelRange.start ), this.toViewPosition( modelRange.end ) );
  256. }
  257. /**
  258. * Gets the corresponding model position.
  259. *
  260. * @fires viewToModelPosition
  261. * @param {module:engine/view/position~Position} viewPosition View position.
  262. * @returns {module:engine/model/position~Position} Corresponding model position.
  263. */
  264. toModelPosition( viewPosition ) {
  265. const data = {
  266. viewPosition,
  267. mapper: this
  268. };
  269. this.fire( 'viewToModelPosition', data );
  270. return data.modelPosition;
  271. }
  272. /**
  273. * Gets the corresponding view position.
  274. *
  275. * @fires modelToViewPosition
  276. * @param {module:engine/model/position~Position} modelPosition Model position.
  277. * @param {Object} [options] Additional options for position mapping process.
  278. * @param {Boolean} [options.isPhantom=false] Should be set to `true` if the model position to map is pointing to a place
  279. * in model tree which no longer exists. For example, it could be an end of a removed model range.
  280. * @returns {module:engine/view/position~Position} Corresponding view position.
  281. */
  282. toViewPosition( modelPosition, options = { isPhantom: false } ) {
  283. const data = {
  284. modelPosition,
  285. mapper: this,
  286. isPhantom: options.isPhantom
  287. };
  288. this.fire( 'modelToViewPosition', data );
  289. return data.viewPosition;
  290. }
  291. /**
  292. * Gets all view elements bound to the given marker name.
  293. *
  294. * @param {String} name Marker name.
  295. * @returns {Set.<module:engine/view/element~Element>|null} View elements bound with given marker name or `null`
  296. * if no elements are bound to given marker name.
  297. */
  298. markerNameToElements( name ) {
  299. const boundElements = this._markerNameToElements.get( name );
  300. if ( !boundElements ) {
  301. return null;
  302. }
  303. const elements = new Set();
  304. for ( const element of boundElements ) {
  305. if ( element.is( 'attributeElement' ) ) {
  306. for ( const clone of element.getElementsWithSameId() ) {
  307. elements.add( clone );
  308. }
  309. } else {
  310. elements.add( element );
  311. }
  312. }
  313. return elements;
  314. }
  315. /**
  316. * Registers a callback that evaluates the length in the model of a view element with given name.
  317. *
  318. * The callback is fired with one argument, which is a view element instance. The callback is expected to return
  319. * a number representing the length of view element in model.
  320. *
  321. * // List item in view may contain nested list, which have other list items. In model though,
  322. * // the lists are represented by flat structure. Because of those differences, length of list view element
  323. * // may be greater than one. In the callback it's checked how many nested list items are in evaluated list item.
  324. *
  325. * function getViewListItemLength( element ) {
  326. * let length = 1;
  327. *
  328. * for ( let child of element.getChildren() ) {
  329. * if ( child.name == 'ul' || child.name == 'ol' ) {
  330. * for ( let item of child.getChildren() ) {
  331. * length += getViewListItemLength( item );
  332. * }
  333. * }
  334. * }
  335. *
  336. * return length;
  337. * }
  338. *
  339. * mapper.registerViewToModelLength( 'li', getViewListItemLength );
  340. *
  341. * @param {String} viewElementName Name of view element for which callback is registered.
  342. * @param {Function} lengthCallback Function return a length of view element instance in model.
  343. */
  344. registerViewToModelLength( viewElementName, lengthCallback ) {
  345. this._viewToModelLengthCallbacks.set( viewElementName, lengthCallback );
  346. }
  347. /**
  348. * For given `viewPosition`, finds and returns the closest ancestor of this position that has a mapping to
  349. * the model.
  350. *
  351. * @param {module:engine/view/position~Position} viewPosition Position for which mapped ancestor should be found.
  352. * @returns {module:engine/view/element~Element}
  353. */
  354. findMappedViewAncestor( viewPosition ) {
  355. let parent = viewPosition.parent;
  356. while ( !this._viewToModelMapping.has( parent ) ) {
  357. parent = parent.parent;
  358. }
  359. return parent;
  360. }
  361. /**
  362. * Calculates model offset based on the view position and the block element.
  363. *
  364. * Example:
  365. *
  366. * <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, p ) -> 5
  367. *
  368. * Is a sum of:
  369. *
  370. * <p>foo|<b>bar</b></p> // _toModelOffset( p, 3, p ) -> 3
  371. * <p>foo<b>ba|r</b></p> // _toModelOffset( b, 2, b ) -> 2
  372. *
  373. * @private
  374. * @param {module:engine/view/element~Element} viewParent Position parent.
  375. * @param {Number} viewOffset Position offset.
  376. * @param {module:engine/view/element~Element} viewBlock Block used as a base to calculate offset.
  377. * @returns {Number} Offset in the model.
  378. */
  379. _toModelOffset( viewParent, viewOffset, viewBlock ) {
  380. if ( viewBlock != viewParent ) {
  381. // See example.
  382. const offsetToParentStart = this._toModelOffset( viewParent.parent, viewParent.index, viewBlock );
  383. const offsetInParent = this._toModelOffset( viewParent, viewOffset, viewParent );
  384. return offsetToParentStart + offsetInParent;
  385. }
  386. // viewBlock == viewParent, so we need to calculate the offset in the parent element.
  387. // If the position is a text it is simple ("ba|r" -> 2).
  388. if ( viewParent.is( 'text' ) ) {
  389. return viewOffset;
  390. }
  391. // If the position is in an element we need to sum lengths of siblings ( <b> bar </b> foo | -> 3 + 3 = 6 ).
  392. let modelOffset = 0;
  393. for ( let i = 0; i < viewOffset; i++ ) {
  394. modelOffset += this.getModelLength( viewParent.getChild( i ) );
  395. }
  396. return modelOffset;
  397. }
  398. /**
  399. * Gets the length of the view element in the model.
  400. *
  401. * The length is calculated as follows:
  402. * * if {@link #registerViewToModelLength length mapping callback} is provided for given `viewNode` it is used to
  403. * evaluate model length (`viewNode` is used as first and only parameter passed to the callback),
  404. * * length of a {@link module:engine/view/text~Text text node} is equal to the length of it's
  405. * {@link module:engine/view/text~Text#data data},
  406. * * length of a {@link module:engine/view/uielement~UIElement ui element} is equal to 0,
  407. * * length of a mapped {@link module:engine/view/element~Element element} is equal to 1,
  408. * * length of a not-mapped {@link module:engine/view/element~Element element} is equal to the length of it's children.
  409. *
  410. * Examples:
  411. *
  412. * foo -> 3 // Text length is equal to it's data length.
  413. * <p>foo</p> -> 1 // Length of an element which is mapped is by default equal to 1.
  414. * <b>foo</b> -> 3 // Length of an element which is not mapped is a length of its children.
  415. * <div><p>x</p><p>y</p></div> -> 2 // Assuming that <div> is not mapped and <p> are mapped.
  416. *
  417. * @param {module:engine/view/element~Element} viewNode View node.
  418. * @returns {Number} Length of the node in the tree model.
  419. */
  420. getModelLength( viewNode ) {
  421. if ( this._viewToModelLengthCallbacks.get( viewNode.name ) ) {
  422. const callback = this._viewToModelLengthCallbacks.get( viewNode.name );
  423. return callback( viewNode );
  424. } else if ( this._viewToModelMapping.has( viewNode ) ) {
  425. return 1;
  426. } else if ( viewNode.is( 'text' ) ) {
  427. return viewNode.data.length;
  428. } else if ( viewNode.is( 'uiElement' ) ) {
  429. return 0;
  430. } else {
  431. let len = 0;
  432. for ( const child of viewNode.getChildren() ) {
  433. len += this.getModelLength( child );
  434. }
  435. return len;
  436. }
  437. }
  438. /**
  439. * Finds the position in the view node (or its children) with the expected model offset.
  440. *
  441. * Example:
  442. *
  443. * <p>fo<b>bar</b>bom</p> -> expected offset: 4
  444. *
  445. * _findPositionIn( p, 4 ):
  446. * <p>|fo<b>bar</b>bom</p> -> expected offset: 4, actual offset: 0
  447. * <p>fo|<b>bar</b>bom</p> -> expected offset: 4, actual offset: 2
  448. * <p>fo<b>bar</b>|bom</p> -> expected offset: 4, actual offset: 5 -> we are too far
  449. *
  450. * _findPositionIn( b, 4 - ( 5 - 3 ) ):
  451. * <p>fo<b>|bar</b>bom</p> -> expected offset: 2, actual offset: 0
  452. * <p>fo<b>bar|</b>bom</p> -> expected offset: 2, actual offset: 3 -> we are too far
  453. *
  454. * _findPositionIn( bar, 2 - ( 3 - 3 ) ):
  455. * We are in the text node so we can simple find the offset.
  456. * <p>fo<b>ba|r</b>bom</p> -> expected offset: 2, actual offset: 2 -> position found
  457. *
  458. * @private
  459. * @param {module:engine/view/element~Element} viewParent Tree view element in which we are looking for the position.
  460. * @param {Number} expectedOffset Expected offset.
  461. * @returns {module:engine/view/position~Position} Found position.
  462. */
  463. _findPositionIn( viewParent, expectedOffset ) {
  464. // Last scanned view node.
  465. let viewNode;
  466. // Length of the last scanned view node.
  467. let lastLength = 0;
  468. let modelOffset = 0;
  469. let viewOffset = 0;
  470. // In the text node it is simple: offset in the model equals offset in the text.
  471. if ( viewParent.is( 'text' ) ) {
  472. return new ViewPosition( viewParent, expectedOffset );
  473. }
  474. // In other cases we add lengths of child nodes to find the proper offset.
  475. // If it is smaller we add the length.
  476. while ( modelOffset < expectedOffset ) {
  477. viewNode = viewParent.getChild( viewOffset );
  478. lastLength = this.getModelLength( viewNode );
  479. modelOffset += lastLength;
  480. viewOffset++;
  481. }
  482. // If it equals we found the position.
  483. if ( modelOffset == expectedOffset ) {
  484. return this._moveViewPositionToTextNode( new ViewPosition( viewParent, viewOffset ) );
  485. }
  486. // If it is higher we need to enter last child.
  487. else {
  488. // ( modelOffset - lastLength ) is the offset to the child we enter,
  489. // so we subtract it from the expected offset to fine the offset in the child.
  490. return this._findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) );
  491. }
  492. }
  493. /**
  494. * Because we prefer positions in text nodes over positions next to text node moves view position to the text node
  495. * if it was next to it.
  496. *
  497. * <p>[]<b>foo</b></p> -> <p>[]<b>foo</b></p> // do not touch if position is not directly next to text
  498. * <p>foo[]<b>foo</b></p> -> <p>foo{}<b>foo</b></p> // move to text node
  499. * <p><b>[]foo</b></p> -> <p><b>{}foo</b></p> // move to text node
  500. *
  501. * @private
  502. * @param {module:engine/view/position~Position} viewPosition Position potentially next to text node.
  503. * @returns {module:engine/view/position~Position} Position in text node if possible.
  504. */
  505. _moveViewPositionToTextNode( viewPosition ) {
  506. // If the position is just after text node, put it at the end of that text node.
  507. // If the position is just before text node, put it at the beginning of that text node.
  508. const nodeBefore = viewPosition.nodeBefore;
  509. const nodeAfter = viewPosition.nodeAfter;
  510. if ( nodeBefore instanceof ViewText ) {
  511. return new ViewPosition( nodeBefore, nodeBefore.data.length );
  512. } else if ( nodeAfter instanceof ViewText ) {
  513. return new ViewPosition( nodeAfter, 0 );
  514. }
  515. // Otherwise, just return the given position.
  516. return viewPosition;
  517. }
  518. /**
  519. * Fired for each model-to-view position mapping request. The purpose of this event is to enable custom model-to-view position
  520. * mapping. Callbacks added to this event take {@link module:engine/model/position~Position model position} and are expected to
  521. * calculate {@link module:engine/view/position~Position view position}. Calculated view position should be added as `viewPosition`
  522. * value in `data` object that is passed as one of parameters to the event callback.
  523. *
  524. * // Assume that "captionedImage" model element is converted to <img> and following <span> elements in view,
  525. * // and the model element is bound to <img> element. Force mapping model positions inside "captionedImage" to that
  526. * // <span> element.
  527. * mapper.on( 'modelToViewPosition', ( evt, data ) => {
  528. * const positionParent = modelPosition.parent;
  529. *
  530. * if ( positionParent.name == 'captionedImage' ) {
  531. * const viewImg = data.mapper.toViewElement( positionParent );
  532. * const viewCaption = viewImg.nextSibling; // The <span> element.
  533. *
  534. * data.viewPosition = new ViewPosition( viewCaption, modelPosition.offset );
  535. *
  536. * // Stop the event if other callbacks should not modify calculated value.
  537. * evt.stop();
  538. * }
  539. * } );
  540. *
  541. * **Note:** keep in mind that sometimes a "phantom" model position is being converted. "Phantom" model position is
  542. * a position that points to a non-existing place in model. Such position might still be valid for conversion, though
  543. * (it would point to a correct place in view when converted). One example of such situation is when a range is
  544. * removed from model, there may be a need to map the range's end (which is no longer valid model position). To
  545. * handle such situation, check `data.isPhantom` flag:
  546. *
  547. * // Assume that there is "customElement" model element and whenever position is before it, we want to move it
  548. * // to the inside of the view element bound to "customElement".
  549. * mapper.on( 'modelToViewPosition', ( evt, data ) => {
  550. * if ( data.isPhantom ) {
  551. * return;
  552. * }
  553. *
  554. * // Below line might crash for phantom position that does not exist in model.
  555. * const sibling = data.modelPosition.nodeBefore;
  556. *
  557. * // Check if this is the element we are interested in.
  558. * if ( !sibling.is( 'customElement' ) ) {
  559. * return;
  560. * }
  561. *
  562. * const viewElement = data.mapper.toViewElement( sibling );
  563. *
  564. * data.viewPosition = new ViewPosition( sibling, 0 );
  565. *
  566. * evt.stop();
  567. * } );
  568. *
  569. * **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
  570. * attach a custom callback after default callback and also use `data.viewPosition` calculated by default callback
  571. * (for example to fix it).
  572. *
  573. * **Note:** default mapping callback will not fire if `data.viewPosition` is already set.
  574. *
  575. * **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
  576. * mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also,
  577. * the condition that checks if special case scenario happened should be as simple as possible.
  578. *
  579. * @event modelToViewPosition
  580. * @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
  581. * `viewPosition` value to that object with calculated {@link module:engine/view/position~Position view position}.
  582. * @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event.
  583. */
  584. /**
  585. * Fired for each view-to-model position mapping request. See {@link module:engine/conversion/mapper~Mapper#event:modelToViewPosition}.
  586. *
  587. * // See example in `modelToViewPosition` event description.
  588. * // This custom mapping will map positions from <span> element next to <img> to the "captionedImage" element.
  589. * mapper.on( 'viewToModelPosition', ( evt, data ) => {
  590. * const positionParent = viewPosition.parent;
  591. *
  592. * if ( positionParent.hasClass( 'image-caption' ) ) {
  593. * const viewImg = positionParent.previousSibling;
  594. * const modelImg = data.mapper.toModelElement( viewImg );
  595. *
  596. * data.modelPosition = new ModelPosition( modelImg, viewPosition.offset );
  597. * evt.stop();
  598. * }
  599. * } );
  600. *
  601. * **Note:** default mapping callback is provided with `low` priority setting and does not cancel the event, so it is possible to
  602. * attach a custom callback after default callback and also use `data.modelPosition` calculated by default callback
  603. * (for example to fix it).
  604. *
  605. * **Note:** default mapping callback will not fire if `data.modelPosition` is already set.
  606. *
  607. * **Note:** these callbacks are called **very often**. For efficiency reasons, it is advised to use them only when position
  608. * mapping between given model and view elements is unsolvable using just elements mapping and default algorithm. Also,
  609. * the condition that checks if special case scenario happened should be as simple as possible.
  610. *
  611. * @event viewToModelPosition
  612. * @param {Object} data Data pipeline object that can store and pass data between callbacks. The callback should add
  613. * `modelPosition` value to that object with calculated {@link module:engine/model/position~Position model position}.
  614. * @param {module:engine/conversion/mapper~Mapper} data.mapper Mapper instance that fired the event.
  615. */
  616. }
  617. mix( Mapper, EmitterMixin );