model-to-view-converters.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import ViewElement from '../view/element';
  6. import ViewAttributeElement from '../view/attributeelement';
  7. import ViewText from '../view/text';
  8. import ViewRange from '../view/range';
  9. import ViewTreeWalker from '../view/treewalker';
  10. import viewWriter from '../view/writer';
  11. import ModelRange from '../model/range';
  12. /**
  13. * Contains {@link module:engine/model/model model} to {@link module:engine/view/view view} converters for
  14. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}.
  15. *
  16. * @module engine/conversion/model-to-view-converters
  17. */
  18. /**
  19. * Function factory, creates a converter that converts node insertion changes from the model to the view.
  20. * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed,
  21. * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the
  22. * dispatcher's {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}.
  23. * It's expected that the function returns a {@link module:engine/view/element~Element}.
  24. * The result of the function will be inserted to the view.
  25. *
  26. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  27. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
  28. *
  29. * modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) );
  30. *
  31. * modelDispatcher.on(
  32. * 'insert:myElem',
  33. * insertElement( ( data, consumable, conversionApi ) => {
  34. * let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) );
  35. *
  36. * // Do something fancy with myElem using data/consumable/conversionApi ...
  37. *
  38. * return myElem;
  39. * }
  40. * ) );
  41. *
  42. * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which
  43. * will be inserted.
  44. * @returns {Function} Insert element event converter.
  45. */
  46. export function insertElement( elementCreator ) {
  47. return ( evt, data, consumable, conversionApi ) => {
  48. const viewElement = ( elementCreator instanceof ViewElement ) ?
  49. elementCreator.clone( true ) :
  50. elementCreator( data, consumable, conversionApi );
  51. if ( !viewElement ) {
  52. return;
  53. }
  54. if ( !consumable.consume( data.item, 'insert' ) ) {
  55. return;
  56. }
  57. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  58. conversionApi.mapper.bindElements( data.item, viewElement );
  59. viewWriter.insert( viewPosition, viewElement );
  60. };
  61. }
  62. /**
  63. * Function factory, creates a default model-to-view converter for text insertion changes.
  64. *
  65. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  66. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  67. *
  68. * modelDispatcher.on( 'insert:$text', insertText() );
  69. *
  70. * @returns {Function} Insert text event converter.
  71. */
  72. export function insertText() {
  73. return ( evt, data, consumable, conversionApi ) => {
  74. if ( !consumable.consume( data.item, 'insert' ) ) {
  75. return;
  76. }
  77. const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  78. const viewText = new ViewText( data.item.data );
  79. viewWriter.insert( viewPosition, viewText );
  80. };
  81. }
  82. /**
  83. * Function factory, creates a converter that converts marker adding change to the view ui element.
  84. * The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}.
  85. * In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning
  86. * and at the end of the range.
  87. *
  88. * **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter
  89. * uses marker as "model source of data". This means that view ui element does not have corresponding model element.
  90. *
  91. * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which
  92. * will be inserted.
  93. * @returns {Function} Insert element event converter.
  94. */
  95. export function insertUIElement( elementCreator ) {
  96. return ( evt, data, consumable, conversionApi ) => {
  97. let viewStartElement, viewEndElement;
  98. if ( elementCreator instanceof ViewElement ) {
  99. viewStartElement = elementCreator.clone( true );
  100. viewEndElement = elementCreator.clone( true );
  101. } else {
  102. data.isOpening = true;
  103. viewStartElement = elementCreator( data, consumable, conversionApi );
  104. data.isOpening = false;
  105. viewEndElement = elementCreator( data, consumable, conversionApi );
  106. }
  107. if ( !viewStartElement || !viewEndElement ) {
  108. return;
  109. }
  110. if ( !consumable.consume( data.range, 'addMarker' ) ) {
  111. return;
  112. }
  113. const mapper = conversionApi.mapper;
  114. viewWriter.insert( mapper.toViewPosition( data.range.start ), viewStartElement );
  115. if ( !data.range.isCollapsed ) {
  116. viewWriter.insert( mapper.toViewPosition( data.range.end ), viewEndElement );
  117. }
  118. };
  119. }
  120. /**
  121. * Function factory, creates a converter that converts set/change attribute changes from the model to the view. Attributes
  122. * from model are converted to the view element attributes in the view. You may provide a custom function to generate a
  123. * key-value attribute pair to add/change. If not provided, model attributes will be converted to view elements attributes
  124. * on 1-to-1 basis.
  125. *
  126. * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
  127. *
  128. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  129. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  130. *
  131. * modelDispatcher.on( 'addAttribute:customAttr:myElem', setAttribute( ( data ) => {
  132. * // Change attribute key from `customAttr` to `class` in view.
  133. * const key = 'class';
  134. * let value = data.attributeNewValue;
  135. *
  136. * // Force attribute value to 'empty' if the model element is empty.
  137. * if ( data.item.childCount === 0 ) {
  138. * value = 'empty';
  139. * }
  140. *
  141. * // Return key-value pair.
  142. * return { key, value };
  143. * } ) );
  144. *
  145. * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  146. * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}.
  147. * The function is passed all the parameters of the
  148. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute}
  149. * or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute} event.
  150. * @returns {Function} Set/change attribute converter.
  151. */
  152. export function setAttribute( attributeCreator ) {
  153. attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) );
  154. return ( evt, data, consumable, conversionApi ) => {
  155. if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
  156. return;
  157. }
  158. const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi );
  159. conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value );
  160. };
  161. }
  162. /**
  163. * Function factory, creates a converter that converts remove attribute changes from the model to the view. Removes attributes
  164. * that were converted to the view element attributes in the view. You may provide a custom function to generate a
  165. * key-value attribute pair to remove. If not provided, model attributes will be removed from view elements on 1-to-1 basis.
  166. *
  167. * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model.
  168. *
  169. * **Note:** You can use the same attribute creator as in {@link module:engine/conversion/model-to-view-converters~setAttribute}.
  170. *
  171. * The converter automatically consumes corresponding value from consumables list and stops the event (see
  172. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  173. *
  174. * modelDispatcher.on( 'removeAttribute:customAttr:myElem', removeAttribute( ( data ) => {
  175. * // Change attribute key from `customAttr` to `class` in view.
  176. * const key = 'class';
  177. * let value = data.attributeNewValue;
  178. *
  179. * // Force attribute value to 'empty' if the model element is empty.
  180. * if ( data.item.childCount === 0 ) {
  181. * value = 'empty';
  182. * }
  183. *
  184. * // Return key-value pair.
  185. * return { key, value };
  186. * } ) );
  187. *
  188. * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which
  189. * represents attribute key and attribute value to be removed from {@link module:engine/view/element~Element view element}.
  190. * The function is passed all the parameters of the
  191. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}
  192. * or {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute changeAttribute event}.
  193. * @returns {Function} Remove attribute converter.
  194. */
  195. export function removeAttribute( attributeCreator ) {
  196. attributeCreator = attributeCreator || ( ( value, key ) => ( { key } ) );
  197. return ( evt, data, consumable, conversionApi ) => {
  198. if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
  199. return;
  200. }
  201. const { key } = attributeCreator( data.attributeOldValue, data.attributeKey, data, consumable, conversionApi );
  202. conversionApi.mapper.toViewElement( data.item ).removeAttribute( key );
  203. };
  204. }
  205. /**
  206. * Function factory, creates a converter that converts set/change attribute changes from the model to the view. In this case,
  207. * model attributes are converted to a view element that will be wrapping view nodes which corresponding model nodes had
  208. * the attribute set. This is useful for attributes like `bold`, which may be set on text nodes in model but are
  209. * represented as an element in the view:
  210. *
  211. * [paragraph] MODEL ====> VIEW <p>
  212. * |- a {bold: true} |- <b>
  213. * |- b {bold: true} | |- ab
  214. * |- c |- c
  215. *
  216. * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and
  217. * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the
  218. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
  219. * It's expected that the function returns a {@link module:engine/view/element~Element}.
  220. * The result of the function will be the wrapping element.
  221. * When provided `Function` does not return element, then will be no conversion.
  222. *
  223. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  224. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  225. *
  226. * modelDispatcher.on( 'addAttribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) );
  227. *
  228. * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
  229. * be used for wrapping.
  230. * @returns {Function} Set/change attribute converter.
  231. */
  232. export function wrapItem( elementCreator ) {
  233. return ( evt, data, consumable, conversionApi ) => {
  234. const viewElement = ( elementCreator instanceof ViewElement ) ?
  235. elementCreator.clone( true ) :
  236. elementCreator( data.attributeNewValue, data, consumable, conversionApi );
  237. if ( !viewElement ) {
  238. return;
  239. }
  240. if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
  241. return;
  242. }
  243. let viewRange = conversionApi.mapper.toViewRange( data.range );
  244. // If this is a change event (because old value is not empty) and the creator is a function (so
  245. // it may create different view elements basing on attribute value) we have to create
  246. // view element basing on old value and unwrap it before wrapping with a newly created view element.
  247. if ( data.attributeOldValue !== null && !( elementCreator instanceof ViewElement ) ) {
  248. const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi );
  249. viewRange = viewWriter.unwrap( viewRange, oldViewElement );
  250. }
  251. viewWriter.wrap( viewRange, viewElement );
  252. };
  253. }
  254. /**
  255. * Function factory, creates a converter that converts remove attribute changes from the model to the view. It assumes, that
  256. * attributes from model were converted to elements in the view. This converter will unwrap view nodes from corresponding
  257. * view element if given attribute was removed.
  258. *
  259. * The view element type that will be unwrapped depends on passed parameter.
  260. * If {@link module:engine/view/element~Element} was passed, it will be used to look for similar element in the view for unwrapping.
  261. * If `Function` is provided, it is passed all the parameters of the
  262. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addAttribute addAttribute event}.
  263. * It's expected that the function returns a {@link module:engine/view/element~Element}.
  264. * The result of the function will be used to look for similar element in the view for unwrapping.
  265. *
  266. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  267. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
  268. *
  269. * modelDispatcher.on( 'removeAttribute:bold', unwrapItem( new ViewAttributeElement( 'strong' ) ) );
  270. *
  271. * @see module:engine/conversion/model-to-view-converters~wrapItem
  272. * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will
  273. * be used for unwrapping.
  274. * @returns {Function} Remove attribute converter.
  275. */
  276. export function unwrapItem( elementCreator ) {
  277. return ( evt, data, consumable, conversionApi ) => {
  278. const viewElement = ( elementCreator instanceof ViewElement ) ?
  279. elementCreator.clone( true ) :
  280. elementCreator( data.attributeOldValue, data, consumable, conversionApi );
  281. if ( !viewElement ) {
  282. return;
  283. }
  284. if ( !consumable.consume( data.item, eventNameToConsumableType( evt.name ) ) ) {
  285. return;
  286. }
  287. const viewRange = conversionApi.mapper.toViewRange( data.range );
  288. viewWriter.unwrap( viewRange, viewElement );
  289. };
  290. }
  291. /**
  292. * Function factory, creates a converter that wraps model range.
  293. *
  294. * In contrary to {@link module:engine/conversion/model-to-view-converters~wrapItem}, this converter's input is a
  295. * {@link module:engine/model/range~Range model range} (not changed model item). The model range is mapped
  296. * to {@link module:engine/view/range~Range view range} and then, view items within that view range are wrapped in a
  297. * {@link module:engine/view/attributeelement~AttributeElement view attribute element}. Note, that `elementCreator`
  298. * function of this converter takes different parameters that `elementCreator` of `wrapItem`.
  299. *
  300. * Let's assume following model and view. `{}` represents a range that is added as a marker with `searchResult` name.
  301. * The range represents a result of search `ab` string in the model document. The range has to be visualized in view.
  302. *
  303. * [paragraph] MODEL ====> VIEW <p>
  304. * |- {a |- <span class="searchResult">
  305. * |- b} | |- ab
  306. * |- c |- c
  307. *
  308. * The wrapping node depends on passed parameter. If {@link module:engine/view/attributeelement~AttributeElement} was passed, it
  309. * will be cloned and the copy will become the wrapping element. If `Function` is provided, it is passed all the parameters of the
  310. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:addMarker addMarker event}. It's expected
  311. * that the function returns a {@link module:engine/view/attributeelement~AttributeElement}. The result of the function will be the
  312. * wrapping element. When provided `Function` does not return element, then will be no conversion.
  313. *
  314. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  315. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}).
  316. *
  317. * modelDispatcher.on( 'addMarker:searchResult', wrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
  318. *
  319. * @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View attribute element, or function returning
  320. * a view attribute element, which will be used for wrapping.
  321. * @returns {Function} Wrap range converter.
  322. */
  323. export function wrapRange( elementCreator ) {
  324. return ( evt, data, consumable, conversionApi ) => {
  325. const viewElement = ( elementCreator instanceof ViewElement ) ?
  326. elementCreator.clone( true ) :
  327. elementCreator( data, consumable, conversionApi );
  328. if ( !viewElement ) {
  329. return;
  330. }
  331. if ( !consumable.consume( data.range, 'addMarker' ) ) {
  332. return;
  333. }
  334. const viewRange = conversionApi.mapper.toViewRange( data.range );
  335. const flatViewRanges = viewWriter.breakViewRangePerContainer( viewRange );
  336. for ( const range of flatViewRanges ) {
  337. viewWriter.wrap( range, viewElement );
  338. }
  339. };
  340. }
  341. /**
  342. * Function factory, creates a converter that converts removing of a model marker to view attribute element.
  343. * This converter will unwrap view nodes from corresponding view range.
  344. *
  345. * The view element that will be unwrapped depends on passed parameter. If {@link module:engine/view/attributeelement~AttributeElement}
  346. * was passed, it will be used to look for similar element in the view for unwrapping. If `Function` is provided, it is passed all
  347. * the parameters of the
  348. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:removeMarker removeMarker event}.
  349. * It's expected that the function returns a {@link module:engine/view/attributeelement~AttributeElement}. The result of
  350. * the function will be used to look for similar element in the view for unwrapping.
  351. *
  352. * The converter automatically consumes corresponding value from consumables list, stops the event (see
  353. * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements.
  354. *
  355. * modelDispatcher.on( 'removeMarker:searchResult', unwrapRange( new ViewAttributeElement( 'span', { class: 'searchResult' } ) ) );
  356. *
  357. * @see module:engine/conversion/model-to-view-converters~wrapRange
  358. * @param {module:engine/view/attributeelement~AttributeElement|Function} elementCreator View attribute element, or function returning
  359. * a view attribute element, which will be used for unwrapping.
  360. * @returns {Function} Unwrap range converter.
  361. */
  362. export function unwrapRange( elementCreator ) {
  363. return ( evt, data, consumable, conversionApi ) => {
  364. const viewElement = ( elementCreator instanceof ViewElement ) ?
  365. elementCreator.clone( true ) :
  366. elementCreator( data, consumable, conversionApi );
  367. if ( !viewElement ) {
  368. return;
  369. }
  370. if ( !consumable.consume( data.range, 'removeMarker' ) ) {
  371. return;
  372. }
  373. const viewRange = conversionApi.mapper.toViewRange( data.range );
  374. const flatViewRanges = viewWriter.breakViewRangePerContainer( viewRange );
  375. for ( const range of flatViewRanges ) {
  376. viewWriter.unwrap( range, viewElement );
  377. }
  378. };
  379. }
  380. /**
  381. * Function factory, creates a default model-to-view converter for node remove changes.
  382. *
  383. * modelDispatcher.on( 'remove', remove() );
  384. *
  385. * @returns {Function} Remove event converter.
  386. */
  387. export function remove() {
  388. return ( evt, data, consumable, conversionApi ) => {
  389. if ( !consumable.consume( data.item, 'remove' ) ) {
  390. return;
  391. }
  392. // We cannot map non-existing positions from model to view. Since a range was removed
  393. // from the model, we cannot recreate that range and map it to view, because
  394. // end of that range is incorrect.
  395. // Instead we will use `data.sourcePosition` as this is the last correct model position and
  396. // it is a position before the removed item. Then, we will calculate view range to remove "manually".
  397. const viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
  398. let viewRange;
  399. if ( data.item.is( 'element' ) ) {
  400. // Note: in remove conversion we cannot use model-to-view element mapping because `data.item` may be
  401. // already mapped to another element (this happens when move change is converted).
  402. // In this case however, `viewPosition` is the position before view element that corresponds to removed model element.
  403. viewRange = ViewRange.createOn( viewPosition.nodeAfter );
  404. } else {
  405. // If removed item is a text node, we need to traverse view tree to find the view range to remove.
  406. // Range to remove will start `viewPosition` and should contain amount of characters equal to the amount of removed characters.
  407. const viewRangeEnd = _shiftViewPositionByCharacters( viewPosition, data.item.offsetSize );
  408. viewRange = new ViewRange( viewPosition, viewRangeEnd );
  409. }
  410. // Trim the range to remove in case some UI elements are on the view range boundaries.
  411. viewWriter.remove( viewRange.getTrimmed() );
  412. // Unbind this element only if it was moved to graveyard.
  413. // The dispatcher#remove event will also be fired if the element was moved to another place (remove+insert are fired).
  414. // Let's say that <b> is moved before <a>. The view will be changed like this:
  415. //
  416. // 1) start: <a></a><b></b>
  417. // 2) insert: <b (new)></b><a></a><b></b>
  418. // 3) remove: <b (new)></b><a></a>
  419. //
  420. // If we'll unbind the <b> element in step 3 we'll also lose binding of the <b (new)> element in the view,
  421. // because unbindModelElement() cancels both bindings – (model <b> => view <b (new)>) and (view <b (new)> => model <b>).
  422. // We can't lose any of these.
  423. //
  424. // See #847.
  425. if ( data.item.root.rootName == '$graveyard' ) {
  426. conversionApi.mapper.unbindModelElement( data.item );
  427. }
  428. };
  429. }
  430. export function virtualSelectionConverter( selectionDescriptor, addMarker = true ) {
  431. return ( evt, data, consumable, conversionApi ) => {
  432. const modelItem = data.item;
  433. const descriptor = typeof selectionDescriptor == 'function' ?
  434. selectionDescriptor( data, consumable, conversionApi ) :
  435. selectionDescriptor;
  436. if ( modelItem.is( 'textProxy' ) ) {
  437. const viewElement = virtualSelectionDescriptorToAttribute( descriptor );
  438. const converter = addMarker ? wrapItem( viewElement ) : unwrapItem( viewElement );
  439. converter( evt, data, consumable, conversionApi );
  440. }
  441. if ( modelItem.is( 'element' ) ) {
  442. const consumableType = eventNameToConsumableType( evt.name );
  443. const viewElement = conversionApi.mapper.toViewElement( modelItem );
  444. if ( !consumable.consume( modelItem, consumableType ) ) {
  445. return;
  446. }
  447. const selectionHandlingMethod = addMarker ? 'setVirtualSelection' : 'removeVirtualSelection';
  448. if ( viewElement && viewElement[ selectionHandlingMethod ] ) {
  449. // Virtual selection will be handled by parent element - consume all children.
  450. for ( const value of ModelRange.createIn( modelItem ) ) {
  451. consumable.consume( value.item, consumableType );
  452. }
  453. viewElement[ selectionHandlingMethod ]( descriptor );
  454. }
  455. }
  456. };
  457. }
  458. // Helper function that shifts given view `position` in a way that returned position is after `howMany` characters compared
  459. // to the original `position`.
  460. // Because in view there might be view ui elements splitting text nodes, we cannot simply use `ViewPosition#getShiftedBy()`.
  461. function _shiftViewPositionByCharacters( position, howMany ) {
  462. // Create a walker that will walk the view tree starting from given position and walking characters one-by-one.
  463. const walker = new ViewTreeWalker( { startPosition: position, singleCharacters: true } );
  464. // We will count visited characters and return the position after `howMany` characters.
  465. let charactersFound = 0;
  466. for ( const value of walker ) {
  467. if ( value.type == 'text' ) {
  468. charactersFound++;
  469. if ( charactersFound == howMany ) {
  470. return walker.position;
  471. }
  472. }
  473. }
  474. }
  475. /**
  476. * Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element}
  477. * basing on marker remove change.
  478. *
  479. * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning
  480. * a view ui element, which will be used as a pattern when look for element to remove at the marker start position.
  481. * @returns {Function} Remove ui element converter.
  482. */
  483. export function removeUIElement( elementCreator ) {
  484. return ( evt, data, consumable, conversionApi ) => {
  485. let viewStartElement, viewEndElement;
  486. if ( elementCreator instanceof ViewElement ) {
  487. viewStartElement = elementCreator.clone( true );
  488. viewEndElement = elementCreator.clone( true );
  489. } else {
  490. data.isOpening = true;
  491. viewStartElement = elementCreator( data, consumable, conversionApi );
  492. data.isOpening = false;
  493. viewEndElement = elementCreator( data, consumable, conversionApi );
  494. }
  495. if ( !viewStartElement || !viewEndElement ) {
  496. return;
  497. }
  498. if ( !consumable.consume( data.range, 'removeMarker' ) ) {
  499. return;
  500. }
  501. const viewRange = conversionApi.mapper.toViewRange( data.range );
  502. // First remove closing element.
  503. viewWriter.clear( viewRange.getEnlarged(), viewEndElement );
  504. // If closing and opening elements are not the same then remove opening element.
  505. if ( !viewStartElement.isSimilar( viewEndElement ) ) {
  506. viewWriter.clear( viewRange.getEnlarged(), viewStartElement );
  507. }
  508. };
  509. }
  510. /**
  511. * Returns the consumable type that is to be consumed in an event, basing on that event name.
  512. *
  513. * @param {String} evtName Event name.
  514. * @returns {String} Consumable type.
  515. */
  516. export function eventNameToConsumableType( evtName ) {
  517. const parts = evtName.split( ':' );
  518. return parts[ 0 ] + ':' + parts[ 1 ];
  519. }
  520. export function virtualSelectionDescriptorToAttribute( descriptor ) {
  521. const attribute = new ViewAttributeElement( 'span', descriptor.attributes );
  522. if ( descriptor.class ) {
  523. attribute.addClass( descriptor.class );
  524. }
  525. return attribute;
  526. }