mapper.js 27 KB

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