converters.js 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module list/converters
  7. */
  8. import ViewListItemElement from './viewlistitemelement';
  9. import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
  10. import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
  11. import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
  12. import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
  13. import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
  14. import ViewTreeWalker from '@ckeditor/ckeditor5-engine/src/view/treewalker';
  15. import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
  16. /**
  17. * A model-to-view converter for `listItem` model element insertion.
  18. *
  19. * It creates a `<ul><li></li><ul>` (or `<ol>`) view structure out of a `listItem` model element, inserts it at the correct
  20. * position, and merges the list with surrounding lists (if available).
  21. *
  22. * @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert
  23. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  24. * @param {Object} data Additional information about the change.
  25. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  26. * @param {Object} conversionApi Conversion interface.
  27. */
  28. export function modelViewInsertion( evt, data, consumable, conversionApi ) {
  29. if ( !consumable.test( data.item, 'insert' ) ||
  30. !consumable.test( data.item, 'addAttribute:type' ) ||
  31. !consumable.test( data.item, 'addAttribute:indent' )
  32. ) {
  33. return;
  34. }
  35. consumable.consume( data.item, 'insert' );
  36. consumable.consume( data.item, 'addAttribute:type' );
  37. consumable.consume( data.item, 'addAttribute:indent' );
  38. const modelItem = data.item;
  39. const viewItem = generateLiInUl( modelItem, conversionApi.mapper );
  40. // Providing kind of "default" insert position in case of converting incorrect model.
  41. const insertPosition = conversionApi.mapper.toViewPosition( ModelPosition.createBefore( modelItem ) );
  42. injectViewList( modelItem, viewItem, conversionApi.mapper, insertPosition );
  43. }
  44. /**
  45. * A model-to-view converter for `type` attribute change on `listItem` model element.
  46. *
  47. * This change means that `<li>` elements parent changes from `<ul>` to `<ol>` (or vice versa). This is accomplished
  48. * by breaking view elements, changing their name and merging them.
  49. *
  50. * @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute
  51. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  52. * @param {Object} data Additional information about the change.
  53. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  54. * @param {Object} conversionApi Conversion interface.
  55. */
  56. export function modelViewChangeType( evt, data, consumable, conversionApi ) {
  57. if ( !consumable.consume( data.item, 'changeAttribute:type' ) ) {
  58. return;
  59. }
  60. const viewItem = conversionApi.mapper.toViewElement( data.item );
  61. // 1. Break the container after and before the list item.
  62. // This will create a view list with one view list item -- the one that changed type.
  63. viewWriter.breakContainer( ViewPosition.createBefore( viewItem ) );
  64. viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
  65. // 2. Change name of the view list that holds the changed view item.
  66. // We cannot just change name property, because that would not render properly.
  67. let viewList = viewItem.parent;
  68. const listName = data.attributeNewValue == 'numbered' ? 'ol' : 'ul';
  69. viewList = viewWriter.rename( viewList, listName );
  70. // 3. Merge the changed view list with other lists, if possible.
  71. mergeViewLists( viewList, viewList.nextSibling );
  72. mergeViewLists( viewList.previousSibling, viewList );
  73. }
  74. /**
  75. * A model-to-view converter for `listItem` model element removal.
  76. *
  77. * @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:remove
  78. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  79. * @param {Object} data Additional information about the change.
  80. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  81. * @param {Object} conversionApi Conversion interface.
  82. */
  83. export function modelViewRemove( evt, data, consumable, conversionApi ) {
  84. if ( !consumable.consume( data.item, 'remove' ) ) {
  85. return;
  86. }
  87. let viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
  88. viewPosition = viewPosition.getLastMatchingPosition( value => !value.item.is( 'li' ) );
  89. const viewItem = viewPosition.nodeAfter;
  90. // 1. Break the container after and before the list item.
  91. // This will create a view list with one view list item -- the one that changed type.
  92. viewWriter.breakContainer( ViewPosition.createBefore( viewItem ) );
  93. viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
  94. // 2. Remove the UL that contains just the removed <li>.
  95. const viewList = viewItem.parent;
  96. const viewListPrev = viewList.previousSibling;
  97. const removeRange = ViewRange.createOn( viewList );
  98. viewWriter.remove( removeRange );
  99. if ( viewListPrev && viewListPrev.nextSibling ) {
  100. mergeViewLists( viewListPrev, viewListPrev.nextSibling );
  101. }
  102. // 3. Bring back nested list that was in the removed <li>.
  103. hoistNestedLists( data.item.getAttribute( 'indent' ) + 1, data.sourcePosition, removeRange.start, viewItem, conversionApi.mapper );
  104. // Unbind this element only if it was moved to graveyard.
  105. // See #847.
  106. if ( data.item.root.rootName == '$graveyard' ) {
  107. conversionApi.mapper.unbindModelElement( data.item );
  108. }
  109. }
  110. /**
  111. * A model-to-view converter for `indent` attribute change on `listItem` model element.
  112. *
  113. * @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:changeAttribute
  114. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  115. * @param {Object} data Additional information about the change.
  116. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  117. * @param {Object} conversionApi Conversion interface.
  118. */
  119. export function modelViewChangeIndent( evt, data, consumable, conversionApi ) {
  120. if ( !consumable.consume( data.item, 'changeAttribute:indent' ) ) {
  121. return;
  122. }
  123. const viewItem = conversionApi.mapper.toViewElement( data.item );
  124. // 1. Break the container after and before the list item.
  125. // This will create a view list with one view list item -- the one that changed type.
  126. viewWriter.breakContainer( ViewPosition.createBefore( viewItem ) );
  127. viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
  128. // 2. Extract view list with changed view list item and merge "hole" possibly created by breaking and removing elements.
  129. const viewList = viewItem.parent;
  130. const viewListPrev = viewList.previousSibling;
  131. const removeRange = ViewRange.createOn( viewList );
  132. viewWriter.remove( removeRange );
  133. // TODO: get rid of `removePosition` when conversion is done on `changesDone`.
  134. let removePosition;
  135. if ( viewListPrev && viewListPrev.nextSibling ) {
  136. removePosition = mergeViewLists( viewListPrev, viewListPrev.nextSibling );
  137. }
  138. if ( !removePosition ) {
  139. removePosition = removeRange.start;
  140. }
  141. // 3. Bring back nested list that was in the removed <li>.
  142. hoistNestedLists( data.attributeOldValue + 1, data.range.start, removeRange.start, viewItem, conversionApi.mapper );
  143. // 4. Inject view list like it is newly inserted.
  144. injectViewList( data.item, viewItem, conversionApi.mapper, removePosition );
  145. }
  146. /**
  147. * A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter is fired for
  148. * insert change of every model item, and should be fired before the actual converter. The converter checks whether the inserted
  149. * model item is a non-`listItem` element. If it is, and it is inserted inside a view list, the converter breaks the
  150. * list so the model element is inserted to the view parent element corresponding to its model parent element.
  151. *
  152. * The converter prevents such situations:
  153. *
  154. * // Model: // View:
  155. * <listItem>foo</listItem> <ul>
  156. * <listItem>bar</listItem> <li>foo</li>
  157. * <li>bar</li>
  158. * </ul>
  159. *
  160. * // After change: // Correct view guaranteed by this converter:
  161. * <listItem>foo</listItem> <ul><li>foo</li></ul><p>xxx</p><ul><li>bar</li></ul>
  162. * <paragraph>xxx</paragraph> // Instead of this wrong view state:
  163. * <listItem>bar</listItem> <ul><li>foo</li><p>xxx</p><li>bar</li></ul>
  164. *
  165. * @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert
  166. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  167. * @param {Object} data Additional information about the change.
  168. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  169. * @param {Object} conversionApi Conversion interface.
  170. */
  171. export function modelViewSplitOnInsert( evt, data, consumable, conversionApi ) {
  172. if ( data.item.name != 'listItem' ) {
  173. let viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
  174. const lists = [];
  175. // Break multiple ULs/OLs if there are.
  176. //
  177. // Imagine following list:
  178. //
  179. // 1 --------
  180. // 1.1 --------
  181. // 1.1.1 --------
  182. // 1.1.2 --------
  183. // 1.1.3 --------
  184. // 1.1.3.1 --------
  185. // 1.2 --------
  186. // 1.2.1 --------
  187. // 2 --------
  188. //
  189. // Insert paragraph after item 1.1.1:
  190. //
  191. // 1 --------
  192. // 1.1 --------
  193. // 1.1.1 --------
  194. //
  195. // Lorem ipsum.
  196. //
  197. // 1.1.2 --------
  198. // 1.1.3 --------
  199. // 1.1.3.1 --------
  200. // 1.2 --------
  201. // 1.2.1 --------
  202. // 2 --------
  203. //
  204. // In this case 1.1.2 has to become beginning of a new list.
  205. // We need to break list before 1.1.2 (obvious), then we need to break list also before 1.2.
  206. // Then we need to move those broken pieces one after another and merge:
  207. //
  208. // 1 --------
  209. // 1.1 --------
  210. // 1.1.1 --------
  211. //
  212. // Lorem ipsum.
  213. //
  214. // 1.1.2 --------
  215. // 1.1.3 --------
  216. // 1.1.3.1 --------
  217. // 1.2 --------
  218. // 1.2.1 --------
  219. // 2 --------
  220. //
  221. while ( viewPosition.parent.name == 'ul' || viewPosition.parent.name == 'ol' ) {
  222. viewPosition = viewWriter.breakContainer( viewPosition );
  223. if ( viewPosition.parent.name != 'li' ) {
  224. break;
  225. }
  226. // Remove lists that are after inserted element.
  227. // They will be brought back later, below the inserted element.
  228. const removeStart = viewPosition;
  229. const removeEnd = ViewPosition.createAt( viewPosition.parent, 'end' );
  230. // Don't remove if there is nothing to remove.
  231. if ( !removeStart.isEqual( removeEnd ) ) {
  232. const removed = viewWriter.remove( new ViewRange( removeStart, removeEnd ) );
  233. lists.push( removed );
  234. }
  235. viewPosition = ViewPosition.createAfter( viewPosition.parent );
  236. }
  237. // Bring back removed lists.
  238. if ( lists.length > 0 ) {
  239. for ( let i = 0; i < lists.length; i++ ) {
  240. const previousList = viewPosition.nodeBefore;
  241. const insertedRange = viewWriter.insert( viewPosition, lists[ i ] );
  242. viewPosition = insertedRange.end;
  243. // Don't merge first list! We want a split in that place (this is why this converter is introduced).
  244. if ( i > 0 ) {
  245. const mergePos = mergeViewLists( previousList, previousList.nextSibling );
  246. // If `mergePos` is in `previousList` it means that the lists got merged.
  247. // In this case, we need to fix insert position.
  248. if ( mergePos && mergePos.parent == previousList ) {
  249. viewPosition.offset--;
  250. }
  251. }
  252. }
  253. // Merge last inserted list with element after it.
  254. mergeViewLists( viewPosition.nodeBefore, viewPosition.nodeAfter );
  255. }
  256. }
  257. }
  258. /**
  259. * A special model-to-view converter introduced by the {@link module:list/list~List list feature}. This converter takes care of
  260. * merging view lists after something is removed or moved from near them.
  261. *
  262. * Example:
  263. *
  264. * // Model: // View:
  265. * <listItem>foo</listItem> <ul><li>foo</li></ul>
  266. * <paragraph>xxx</paragraph> <p>xxx</p>
  267. * <listItem>bar</listItem> <ul><li>bar</li></ul>
  268. *
  269. * // After change: // Correct view guaranteed by this converter:
  270. * <listItem>foo</listItem> <ul>
  271. * <listItem>bar</listItem> <li>foo</li>
  272. * <li>bar</li>
  273. * </ul>
  274. *
  275. * @see module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:remove
  276. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  277. * @param {Object} data Additional information about the change.
  278. * @param {module:engine/conversion/modelconsumable~ModelConsumable} consumable Values to consume.
  279. * @param {Object} conversionApi Conversion interface.
  280. */
  281. export function modelViewMergeAfter( evt, data, consumable, conversionApi ) {
  282. if ( !data.item.is( 'listItem' ) ) {
  283. const viewPosition = conversionApi.mapper.toViewPosition( data.sourcePosition );
  284. const viewItemPrev = viewPosition.nodeBefore;
  285. const viewItemNext = viewPosition.nodeAfter;
  286. // Merge lists if something (remove, move) was done from inside of list.
  287. // Merging will be done only if both items are view lists of the same type.
  288. // The check is done inside the helper function.
  289. mergeViewLists( viewItemPrev, viewItemNext );
  290. }
  291. }
  292. /**
  293. * A view-to-model converter that converts `<li>` view elements into `listItem` model elements.
  294. *
  295. * To set correct values of the `type` and `indent` attributes the converter:
  296. * * checks `<li>`'s parent,
  297. * * passes the `data.indent` value when `<li>`'s sub-items are converted.
  298. *
  299. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  300. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  301. * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
  302. * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
  303. * @param {Object} conversionApi Conversion interface to be used by the callback.
  304. */
  305. export function viewModelConverter( evt, data, consumable, conversionApi ) {
  306. if ( consumable.consume( data.input, { name: true } ) ) {
  307. const batch = conversionApi.batch;
  308. // 1. Create `listItem` model element.
  309. const listItem = batch.createElement( 'listItem' );
  310. // 2. Handle `listItem` model element attributes.
  311. data.indent = data.indent ? data.indent : 0;
  312. batch.setAttribute( 'indent', data.indent, listItem );
  313. // Set 'bulleted' as default. If this item is pasted into a context,
  314. const type = data.input.parent && data.input.parent.name == 'ol' ? 'numbered' : 'bulleted';
  315. batch.setAttribute( 'type', type, listItem );
  316. // 3. Handle `<li>` children.
  317. data.context.push( listItem );
  318. // `listItem`s created recursively should have bigger indent.
  319. data.indent++;
  320. // `listItem`s will be kept in flat structure.
  321. const items = batch.createDocumentFragment();
  322. batch.append( listItem, items );
  323. // Check all children of the converted `<li>`.
  324. // At this point we assume there are no "whitespace" view text nodes in view list, between view list items.
  325. // This should be handled by `<ul>` and `<ol>` converters.
  326. for ( const child of data.input.getChildren() ) {
  327. // Let's convert the child.
  328. const converted = conversionApi.convertItem( child, consumable, data );
  329. // If this is a view list element, we will convert it and concat the result (`listItem` model elements)
  330. // with already gathered results (in `items` array). `converted` should be a `ModelDocumentFragment`.
  331. if ( child.name == 'ul' || child.name == 'ol' ) {
  332. batch.append( converted, items );
  333. }
  334. // If it was not a list it was a "regular" list item content. Just append it to `listItem`.
  335. else {
  336. batch.append( converted, listItem );
  337. }
  338. }
  339. data.indent--;
  340. data.context.pop();
  341. data.output = items;
  342. }
  343. }
  344. /**
  345. * A view-to-model converter for `<ul>` and `<ol>` view elements that cleans the input view of garbage.
  346. * This is mostly to clean whitespaces from between `<li>` view elements inside the view list element, however, also
  347. * incorrect data can be cleared if the view was incorrect.
  348. *
  349. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  350. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  351. * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
  352. * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
  353. */
  354. export function cleanList( evt, data, consumable ) {
  355. if ( consumable.test( data.input, { name: true } ) ) {
  356. // Caching children because when we start removing them iterating fails.
  357. const children = Array.from( data.input.getChildren() );
  358. for ( const child of children ) {
  359. if ( !child.is( 'li' ) ) {
  360. child.remove();
  361. }
  362. }
  363. }
  364. }
  365. /**
  366. * A view-to-model converter for `<li>` elements that cleans whitespace formatting from the input view.
  367. *
  368. * @see module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#event:element
  369. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  370. * @param {Object} data An object containing conversion input and a placeholder for conversion output and possibly other values.
  371. * @param {module:engine/conversion/viewconsumable~ViewConsumable} consumable Values to consume.
  372. */
  373. export function cleanListItem( evt, data, consumable ) {
  374. if ( consumable.test( data.input, { name: true } ) ) {
  375. if ( data.input.childCount === 0 ) {
  376. return;
  377. }
  378. const children = [ ...data.input.getChildren() ];
  379. let foundList = false;
  380. let firstNode = true;
  381. for ( const child of children ) {
  382. if ( foundList && !child.is( 'ul' ) && !child.is( 'ol' ) ) {
  383. child.remove();
  384. }
  385. if ( child.is( 'text' ) ) {
  386. // If this is the first node and it's a text node, left-trim it.
  387. if ( firstNode ) {
  388. child.data = child.data.replace( /^\s+/, '' );
  389. }
  390. // If this is the last text node before <ul> or <ol>, right-trim it.
  391. if ( !child.nextSibling || ( child.nextSibling.is( 'ul' ) || child.nextSibling.is( 'ol' ) ) ) {
  392. child.data = child.data.replace( /\s+$/, '' );
  393. }
  394. } else if ( child.is( 'ul' ) || child.is( 'ol' ) ) {
  395. // If this is a <ul> or <ol>, do not process it, just mark that we already visited list element.
  396. foundList = true;
  397. }
  398. firstNode = false;
  399. }
  400. }
  401. }
  402. /**
  403. * The callback for model position to view position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
  404. * positions between `listItem` elements that would be incorrectly mapped because of how list items are represented in model
  405. * and view.
  406. *
  407. * @see module:engine/conversion/mapper~Mapper#event:modelToViewPosition
  408. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  409. * @param {Object} data An object containing additional data and placeholder for mapping result.
  410. */
  411. export function modelToViewPosition( evt, data ) {
  412. const modelItem = data.modelPosition.nodeBefore;
  413. if ( modelItem && modelItem.is( 'listItem' ) ) {
  414. const viewItem = data.mapper.toViewElement( modelItem );
  415. const topmostViewList = viewItem.getAncestors().find( element => element.is( 'ul' ) || element.is( 'ol' ) );
  416. const walker = new ViewTreeWalker( {
  417. startPosition: ViewPosition.createAt( viewItem, 0 )
  418. } );
  419. for ( const value of walker ) {
  420. if ( value.type == 'elementStart' && value.item.is( 'li' ) ) {
  421. data.viewPosition = value.previousPosition;
  422. break;
  423. } else if ( value.type == 'elementEnd' && value.item == topmostViewList ) {
  424. data.viewPosition = value.nextPosition;
  425. break;
  426. }
  427. }
  428. }
  429. }
  430. /**
  431. * The callback for view position to model position mapping for {@link module:engine/conversion/mapper~Mapper}. The callback fixes
  432. * positions between `<li>` elements that would be incorrectly mapped because of how list items are represented in model
  433. * and view.
  434. *
  435. * @see module:engine/conversion/mapper~Mapper#event:viewToModelPosition
  436. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  437. * @param {Object} data An object containing additional data and placeholder for mapping result.
  438. */
  439. export function viewToModelPosition( evt, data ) {
  440. const viewPos = data.viewPosition;
  441. const viewParent = viewPos.parent;
  442. const mapper = data.mapper;
  443. if ( viewParent.name == 'ul' || viewParent.name == 'ol' ) {
  444. // Position is directly in <ul> or <ol>.
  445. if ( !viewPos.isAtEnd ) {
  446. // If position is not at the end, it must be before <li>.
  447. // Get that <li>, map it to `listItem` and set model position before that `listItem`.
  448. const modelNode = mapper.toModelElement( viewPos.nodeAfter );
  449. data.modelPosition = ModelPosition.createBefore( modelNode );
  450. } else {
  451. // Position is at the end of <ul> or <ol>, so there is no <li> after it to be mapped.
  452. // There is <li> before the position, but we cannot just map it to `listItem` and set model position after it,
  453. // because that <li> may contain nested items.
  454. // We will check "model length" of that <li>, in other words - how many `listItem`s are in that <li>.
  455. const modelNode = mapper.toModelElement( viewPos.nodeBefore );
  456. const modelLength = mapper.getModelLength( viewPos.nodeBefore );
  457. // Then we get model position before mapped `listItem` and shift it accordingly.
  458. data.modelPosition = ModelPosition.createBefore( modelNode ).getShiftedBy( modelLength );
  459. }
  460. evt.stop();
  461. } else if ( viewParent.name == 'li' && viewPos.nodeBefore && ( viewPos.nodeBefore.name == 'ul' || viewPos.nodeBefore.name == 'ol' ) ) {
  462. // In most cases when view position is in <li> it is in text and this is a correct position.
  463. // However, if position is after <ul> or <ol> we have to fix it -- because in model <ul>/<ol> are not in the `listItem`.
  464. const modelNode = mapper.toModelElement( viewParent );
  465. // Check all <ul>s and <ol>s that are in the <li> but before mapped position.
  466. // Get model length of those elements and then add it to the offset of `listItem` mapped to the original <li>.
  467. let modelLength = 1; // Starts from 1 because the original <li> has to be counted in too.
  468. let viewList = viewPos.nodeBefore;
  469. while ( viewList && ( viewList.is( 'ul' ) || viewList.is( 'ol' ) ) ) {
  470. modelLength += mapper.getModelLength( viewList );
  471. viewList = viewList.previousSibling;
  472. }
  473. data.modelPosition = ModelPosition.createBefore( modelNode ).getShiftedBy( modelLength );
  474. evt.stop();
  475. }
  476. }
  477. /**
  478. * Post-fixer that reacts to changes on document and fixes incorrect model states.
  479. *
  480. * Example:
  481. *
  482. * <listItem type="bulleted" indent=0>Item 1</listItem>
  483. * <listItem type="bulleted" indent=1>Item 2</listItem> <--- this is removed.
  484. * <listItem type="bulleted" indent=2>Item 3</listItem>
  485. *
  486. * Should become:
  487. *
  488. * <listItem type="bulleted" indent=0>Item 1</listItem>
  489. * <listItem type="bulleted" indent=1>Item 3</listItem> <--- note that indent got post-fixed.
  490. *
  491. * @param {module:engine/model/document~Document} document The document to observe.
  492. * @returns {Function} A callback to be attached to the {@link module:engine/model/document~Document#event:change document change event}.
  493. */
  494. export function modelChangePostFixer( document ) {
  495. return ( evt, type, changes, batch ) => {
  496. if ( batch.type == 'transparent' ) {
  497. return;
  498. }
  499. if ( type == 'remove' ) {
  500. const howMany = changes.range.end.offset - changes.range.start.offset;
  501. const sourcePos = changes.sourcePosition._getTransformedByInsertion( changes.range.start, howMany, true );
  502. // Fix list items after the cut-out range.
  503. // This fix is needed if items in model after cut-out range have now wrong indents compared to their previous siblings.
  504. _fixItemsIndent( sourcePos, document, batch );
  505. // This fix is needed if two different nested lists got merged, change types of list items "below".
  506. _fixItemsType( sourcePos, false, document, batch );
  507. } else if ( type == 'move' ) {
  508. const howMany = changes.range.end.offset - changes.range.start.offset;
  509. const sourcePos = changes.sourcePosition._getTransformedByInsertion( changes.range.start, howMany, true );
  510. // Fix list items after the cut-out range.
  511. // This fix is needed if items in model after cut-out range have now wrong indents compared to their previous siblings.
  512. _fixItemsIndent( sourcePos, document, batch );
  513. // This fix is needed if two different nested lists got merged, change types of list items "below".
  514. _fixItemsType( sourcePos, false, document, batch );
  515. // Fix items in moved range.
  516. // This fix is needed if inserted items are too deeply intended.
  517. _fixItemsIndent( changes.range.start, document, batch );
  518. // This fix is needed if one or more first inserted items have different type.
  519. _fixItemsType( changes.range.start, false, document, batch );
  520. // Fix list items after inserted range.
  521. // This fix is needed if items in model after inserted range have wrong indents.
  522. _fixItemsIndent( changes.range.end, document, batch );
  523. // This fix is needed if one or more last inserted items have different type.
  524. _fixItemsType( changes.range.end, true, document, batch );
  525. } else if ( type == 'rename' && changes.oldName == 'listItem' && changes.newName != 'listItem' ) {
  526. const element = changes.element;
  527. // Element name is changed from list to something else. Remove useless attributes.
  528. document.enqueueChanges( () => {
  529. batch.removeAttribute( 'indent', element );
  530. batch.removeAttribute( 'type', element );
  531. } );
  532. const changePos = ModelPosition.createAfter( changes.element );
  533. // Fix list items after the renamed element.
  534. // This fix is needed if there are items after renamed element, those items should start from indent = 0.
  535. _fixItemsIndent( changePos, document, batch );
  536. } else if ( type == 'insert' ) {
  537. // Fix list items in inserted range.
  538. // This fix is needed if inserted items are too deeply intended.
  539. _fixItemsIndent( changes.range.start, document, batch );
  540. // This fix is needed if one or more first inserted items have different type.
  541. _fixItemsType( changes.range.start, false, document, batch );
  542. // Fix list items after inserted range.
  543. // This fix is needed if items in model after inserted range have wrong indents.
  544. _fixItemsIndent( changes.range.end, document, batch );
  545. // This fix is needed if one or more last inserted items have different type.
  546. _fixItemsType( changes.range.end, true, document, batch );
  547. }
  548. };
  549. }
  550. // Helper function for post fixer callback. Performs fixing of model `listElement` items indent attribute. Checks the model at the
  551. // `changePosition`. Looks at the node before position where change occurred and uses that node as a reference for following list items.
  552. function _fixItemsIndent( changePosition, document, batch ) {
  553. let nextItem = changePosition.nodeAfter;
  554. if ( nextItem && nextItem.name == 'listItem' ) {
  555. document.enqueueChanges( () => {
  556. const prevItem = nextItem.previousSibling;
  557. // This is the maximum indent that following model list item may have.
  558. const maxIndent = prevItem && prevItem.is( 'listItem' ) ? prevItem.getAttribute( 'indent' ) + 1 : 0;
  559. // Check how much the next item needs to be outdented.
  560. let outdentBy = nextItem.getAttribute( 'indent' ) - maxIndent;
  561. const items = [];
  562. while ( nextItem && nextItem.name == 'listItem' && nextItem.getAttribute( 'indent' ) > maxIndent ) {
  563. if ( outdentBy > nextItem.getAttribute( 'indent' ) ) {
  564. outdentBy = nextItem.getAttribute( 'indent' );
  565. }
  566. const newIndent = nextItem.getAttribute( 'indent' ) - outdentBy;
  567. items.push( { item: nextItem, indent: newIndent } );
  568. nextItem = nextItem.nextSibling;
  569. }
  570. if ( items.length > 0 ) {
  571. // Since we are outdenting list items, it is safer to start from the last one (it will maintain correct model state).
  572. for ( const item of items.reverse() ) {
  573. batch.setAttribute( 'indent', item.indent, item.item );
  574. }
  575. }
  576. } );
  577. }
  578. }
  579. // Helper function for post fixer callback. Performs fixing of model nested `listElement` items type attribute.
  580. // Checks the model at the `changePosition`. Looks at nodes after/before that position and changes those items type
  581. // to the same as node before/after `changePosition`.
  582. function _fixItemsType( changePosition, fixPrevious, document, batch ) {
  583. let item = changePosition[ fixPrevious ? 'nodeBefore' : 'nodeAfter' ];
  584. if ( !item || !item.is( 'listItem' ) || item.getAttribute( 'indent' ) === 0 ) {
  585. // !item - when last item got removed.
  586. // !item.is( 'listItem' ) - when first element to fix is not a list item already.
  587. // indent === 0 - do not fix if changes are done on top level lists.
  588. return;
  589. }
  590. document.enqueueChanges( () => {
  591. const refItem = _getBoundaryItemOfSameList( item, !fixPrevious );
  592. if ( !refItem || refItem == item ) {
  593. // !refItem - happens if first list item is inserted.
  594. // refItem == item - happens if last item is inserted.
  595. return;
  596. }
  597. const refIndent = refItem.getAttribute( 'indent' );
  598. const refType = refItem.getAttribute( 'type' );
  599. while ( item && item.is( 'listItem' ) && item.getAttribute( 'indent' ) >= refIndent ) {
  600. if ( item.getAttribute( 'type' ) != refType && item.getAttribute( 'indent' ) == refIndent ) {
  601. batch.setAttribute( 'type', refType, item );
  602. }
  603. item = item[ fixPrevious ? 'previousSibling' : 'nextSibling' ];
  604. }
  605. } );
  606. }
  607. /**
  608. * A fixer for pasted content that includes list items.
  609. *
  610. * It fixes indentation of pasted list items so the pasted items match correctly to the context they are pasted into.
  611. *
  612. * Example:
  613. *
  614. * <listItem type="bulleted" indent=0>A</listItem>
  615. * <listItem type="bulleted" indent=1>B^</listItem>
  616. * // At ^ paste: <listItem type="bulleted" indent=4>X</listItem>
  617. * // <listItem type="bulleted" indent=5>Y</listItem>
  618. * <listItem type="bulleted" indent=2>C</listItem>
  619. *
  620. * Should become:
  621. *
  622. * <listItem type="bulleted" indent=0>A</listItem>
  623. * <listItem type="bulleted" indent=1>BX</listItem>
  624. * <listItem type="bulleted" indent=2>Y/listItem>
  625. * <listItem type="bulleted" indent=2>C</listItem>
  626. *
  627. * @param {module:utils/eventinfo~EventInfo} evt An object containing information about the fired event.
  628. * @param {Array} args Arguments of {@link module:engine/controller/datacontroller~DataController#insertContent}.
  629. */
  630. export function modelIndentPasteFixer( evt, [ content, selection ] ) {
  631. // Check whether inserted content starts from a `listItem`. If it does not, it means that there are some other
  632. // elements before it and there is no need to fix indents, because even if we insert that content into a list,
  633. // that list will be broken.
  634. // Note: we also need to handle singular elements because inserting item with indent 0 into 0,1,[],2
  635. // would create incorrect model.
  636. let item = content.is( 'documentFragment' ) ? content.getChild( 0 ) : content;
  637. if ( item && item.is( 'listItem' ) ) {
  638. // Get a reference list item. Inserted list items will be fixed according to that item.
  639. const pos = selection.getFirstPosition();
  640. let refItem = null;
  641. if ( pos.parent.is( 'listItem' ) ) {
  642. refItem = pos.parent;
  643. } else if ( pos.nodeBefore && pos.nodeBefore.is( 'listItem' ) ) {
  644. refItem = pos.nodeBefore;
  645. }
  646. // If there is `refItem` it means that we do insert list items into an existing list.
  647. if ( refItem ) {
  648. // First list item in `data` has indent equal to 0 (it is a first list item). It should have indent equal
  649. // to the indent of reference item. We have to fix the first item and all of it's children and following siblings.
  650. // Indent of all those items has to be adjusted to reference item.
  651. const indentChange = refItem.getAttribute( 'indent' );
  652. // Fix only if there is anything to fix.
  653. if ( indentChange > 0 ) {
  654. // Adjust indent of all "first" list items in inserted data.
  655. while ( item && item.is( 'listItem' ) ) {
  656. item.setAttribute( 'indent', item.getAttribute( 'indent' ) + indentChange );
  657. item = item.nextSibling;
  658. }
  659. }
  660. }
  661. }
  662. }
  663. // Helper function that creates a `<ul><li></li></ul>` or (`<ol>`) structure out of given `modelItem` model `listItem` element.
  664. // Then, it binds created view list item (<li>) with model `listItem` element.
  665. // The function then returns created view list item (<li>).
  666. function generateLiInUl( modelItem, mapper ) {
  667. const listType = modelItem.getAttribute( 'type' ) == 'numbered' ? 'ol' : 'ul';
  668. const viewItem = new ViewListItemElement();
  669. const viewList = new ViewContainerElement( listType, null );
  670. viewList.appendChildren( viewItem );
  671. mapper.bindElements( modelItem, viewItem );
  672. return viewItem;
  673. }
  674. // Helper function that seeks for a list item sibling of given model item (or position) which meets given criteria.
  675. // `options` object may contain one or more of given values (by default they are `false`):
  676. // `options.getNext` - whether next or previous siblings should be checked (default = previous)
  677. // `options.checkAllSiblings` - whether all siblings or just the first one should be checked (default = only one),
  678. // `options.sameIndent` - whether sought sibling should have same indent (default = no),
  679. // `options.biggerIndent` - whether sought sibling should have bigger indent (default = no).
  680. // `options.smallerIndent` - whether sought sibling should have smaller indent (default = no).
  681. // `options.isMapped` - whether sought sibling must be mapped to view (default = no).
  682. // `options.mapper` - used to map model elements when `isMapped` option is set to true.
  683. // `options.indent` - used as reference item when first parameter is a position
  684. // Either `options.sameIndent` or `options.biggerIndent` should be set to `true`.
  685. function getSiblingListItem( modelItemOrPosition, options ) {
  686. const direction = options.getNext ? 'nextSibling' : 'previousSibling';
  687. const posDirection = options.getNext ? 'nodeAfter' : 'nodeBefore';
  688. const checkAllSiblings = !!options.checkAllSiblings;
  689. const sameIndent = !!options.sameIndent;
  690. const biggerIndent = !!options.biggerIndent;
  691. const smallerIndent = !!options.smallerIndent;
  692. const isMapped = !!options.isMapped;
  693. const indent = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition.getAttribute( 'indent' ) : options.indent;
  694. let item = modelItemOrPosition instanceof ModelElement ? modelItemOrPosition[ direction ] : modelItemOrPosition[ posDirection ];
  695. while ( item && item.name == 'listItem' ) {
  696. const itemIndent = item.getAttribute( 'indent' );
  697. if (
  698. ( sameIndent && indent == itemIndent ) ||
  699. ( biggerIndent && indent < itemIndent ) ||
  700. ( smallerIndent && indent > itemIndent )
  701. ) {
  702. if ( !isMapped || options.mapper.toViewElement( item ) ) {
  703. return item;
  704. } else {
  705. item = item[ direction ];
  706. continue;
  707. }
  708. }
  709. if ( !checkAllSiblings ) {
  710. return null;
  711. }
  712. item = item[ direction ];
  713. }
  714. return null;
  715. }
  716. // Helper function that takes two parameters, that are expected to be view list elements, and merges them.
  717. // The merge happen only if both parameters are UL or OL elements.
  718. function mergeViewLists( firstList, secondList ) {
  719. if ( firstList && secondList && ( firstList.name == 'ul' || firstList.name == 'ol' ) && firstList.name == secondList.name ) {
  720. return viewWriter.mergeContainers( ViewPosition.createAfter( firstList ) );
  721. }
  722. return null;
  723. }
  724. // Helper function that takes model list item element `modelItem`, corresponding view list item element `injectedItem`
  725. // that is not added to the view and is inside a view list element (`ul` or `ol`) and is that's list only child.
  726. // The list is inserted at correct position (element breaking may be needed) and then merged with it's siblings.
  727. // See comments below to better understand the algorithm.
  728. function injectViewList( modelItem, injectedItem, mapper, removePosition ) {
  729. const injectedList = injectedItem.parent;
  730. // Position where view list will be inserted.
  731. let insertPosition;
  732. // 1. Find previous list item that has same or smaller indent. Basically we are looking for a first model item
  733. // that is "parent" or "sibling" if injected model item.
  734. // If there is no such list item, it means that injected list item is the first item in "its list".
  735. let prevItem = getSiblingListItem( modelItem, { sameIndent: true, smallerIndent: true, checkAllSiblings: true } );
  736. if ( prevItem && prevItem.getAttribute( 'indent' ) == modelItem.getAttribute( 'indent' ) ) {
  737. // There is a list item with same indent - we found same-level sibling.
  738. // Break the list after it. Inserted view item will be inserted in the broken space.
  739. const viewItem = mapper.toViewElement( prevItem );
  740. insertPosition = viewWriter.breakContainer( ViewPosition.createAfter( viewItem ) );
  741. } else {
  742. // There is no list item with same indent. Check previous model item.
  743. prevItem = modelItem.previousSibling;
  744. if ( prevItem && prevItem.name == 'listItem' ) {
  745. // // If it is a list item, it has to have lower indent.
  746. // // It means that inserted item should be added to it as its nested item.
  747. // insertPosition = mapper.toViewPosition( ModelPosition.createAt( prevItem, 'end' ) );
  748. // ^ ACTUALLY NOT BECAUSE FIXING DOES NOT WORK PROPERLY.
  749. // TODO: fix this part of code when conversion from model to view is done on `changesDone` event or post/prefixing is better.
  750. if ( prevItem.getAttribute( 'indent' ) < modelItem.getAttribute( 'indent' ) ) {
  751. // Lower indent, correct model, previous item is a parent and this model item is its nested item.
  752. insertPosition = mapper.toViewPosition( ModelPosition.createAt( prevItem, 'end' ) );
  753. } else {
  754. // Higher indent, incorrect model that is probably being fixed. Inject the view list where it was.
  755. // TODO: get rid of `removePosition` when conversion is done on `changesDone`.
  756. if ( removePosition.parent.is( 'ul' ) || removePosition.parent.is( 'ol' ) ) {
  757. insertPosition = viewWriter.breakContainer( removePosition );
  758. } else {
  759. insertPosition = removePosition;
  760. }
  761. }
  762. } else {
  763. // Previous item is not a list item (or does not exist at all).
  764. // Just map the position and insert the view item at mapped position.
  765. insertPosition = mapper.toViewPosition( ModelPosition.createBefore( modelItem ) );
  766. }
  767. }
  768. insertPosition = positionAfterUiElements( insertPosition );
  769. // Insert the view item.
  770. viewWriter.insert( insertPosition, injectedList );
  771. // 2. Handle possible children of injected model item.
  772. // We have to check if next list item in model has bigger indent. If it has, it means that it and possibly
  773. // some following list items should be nested in the injected view item.
  774. // Look only after model elements that are already mapped to view. Some following model items might not be mapped
  775. // if multiple items in model were inserted/moved at once.
  776. const nextItem = getSiblingListItem(
  777. modelItem,
  778. { biggerIndent: true, getNext: true, isMapped: true, mapper }
  779. );
  780. if ( nextItem ) {
  781. const viewItem = mapper.toViewElement( nextItem );
  782. // Break the list between found view item and its preceding `<li>`s.
  783. viewWriter.breakContainer( ViewPosition.createBefore( viewItem ) );
  784. // The broken ("lower") part will be moved as nested children of the inserted view item.
  785. const sourceStart = ViewPosition.createBefore( viewItem.parent );
  786. const lastModelItem = _getBoundaryItemOfSameList( nextItem, false );
  787. const lastViewItem = mapper.toViewElement( lastModelItem );
  788. const sourceEnd = viewWriter.breakContainer( ViewPosition.createAfter( lastViewItem ) );
  789. const sourceRange = new ViewRange( sourceStart, sourceEnd );
  790. const targetPosition = ViewPosition.createAt( injectedItem, 'end' );
  791. viewWriter.move( sourceRange, targetPosition );
  792. }
  793. // Merge inserted view list with its possible neighbour lists.
  794. mergeViewLists( injectedList, injectedList.nextSibling );
  795. mergeViewLists( injectedList.previousSibling, injectedList );
  796. }
  797. // Helper function that takes all children of given `viewRemovedItem` and moves them in a correct place, according
  798. // to other given parameters.
  799. function hoistNestedLists( nextIndent, modelRemoveStartPosition, viewRemoveStartPosition, viewRemovedItem, mapper ) {
  800. // Find correct previous model list item element.
  801. // The element has to have either same or smaller indent than given reference indent.
  802. // This will be the model element which will get nested items (if it has smaller indent) or sibling items (if it has same indent).
  803. // Keep in mind that such element might not be found, if removed item was the first item.
  804. const prevModelItem = getSiblingListItem( modelRemoveStartPosition, {
  805. sameIndent: true,
  806. smallerIndent: true,
  807. checkAllSiblings: true,
  808. indent: nextIndent
  809. } );
  810. // Indent of found element or `null` if the element has not been found.
  811. const prevIndent = prevModelItem ? prevModelItem.getAttribute( 'indent' ) : null;
  812. let insertPosition;
  813. if ( !prevModelItem ) {
  814. // If element has not been found, simply insert lists at the position where the removed item was:
  815. //
  816. // Lorem ipsum.
  817. // 1 -------- <--- this is removed, no previous list item, put nested items in place of removed item.
  818. // 1.1 -------- <--- this is reference indent.
  819. // 1.1.1 --------
  820. // 1.1.2 --------
  821. // 1.2 --------
  822. //
  823. // Becomes:
  824. //
  825. // Lorem ipsum.
  826. // 1.1 --------
  827. // 1.1.1 --------
  828. // 1.1.2 --------
  829. // 1.2 --------
  830. insertPosition = viewRemoveStartPosition;
  831. } else if ( prevIndent == nextIndent ) {
  832. // If element has been found and has same indent as reference indent it means that nested items should
  833. // become siblings of found element:
  834. //
  835. // 1 --------
  836. // 1.1 --------
  837. // 1.2 -------- <--- this is `prevModelItem`.
  838. // 2 -------- <--- this is removed, previous list item has indent same as reference indent.
  839. // 2.1 -------- <--- this is reference indent, this and 2.2 should become siblings of 1.2.
  840. // 2.2 --------
  841. //
  842. // Becomes:
  843. //
  844. // 1 --------
  845. // 1.1 --------
  846. // 1.2 --------
  847. // 2.1 --------
  848. // 2.2 --------
  849. const prevViewList = mapper.toViewElement( prevModelItem ).parent;
  850. insertPosition = ViewPosition.createAfter( prevViewList );
  851. } else {
  852. // If element has been found and has smaller indent as reference indent it means that nested items
  853. // should become nested items of found item:
  854. //
  855. // 1 -------- <--- this is `prevModelItem`.
  856. // 1.1 -------- <--- this is removed, previous list item has indent smaller than reference indent.
  857. // 1.1.1 -------- <--- this is reference indent, this and 1.1.1 should become nested items of 1.
  858. // 1.1.2 --------
  859. // 1.2 --------
  860. //
  861. // Becomes:
  862. //
  863. // 1 --------
  864. // 1.1.1 --------
  865. // 1.1.2 --------
  866. // 1.2 --------
  867. //
  868. // Note: in this case 1.1.1 have indent 2 while 1 have indent 0. In model that should not be possible,
  869. // because following item may have indent bigger only by one. But this is fixed by postfixer.
  870. const modelPosition = ModelPosition.createAt( prevModelItem, 'end' );
  871. insertPosition = mapper.toViewPosition( modelPosition );
  872. }
  873. insertPosition = positionAfterUiElements( insertPosition );
  874. // Handle multiple lists. This happens if list item has nested numbered and bulleted lists. Following lists
  875. // are inserted after the first list (no need to recalculate insertion position for them).
  876. for ( const child of [ ...viewRemovedItem.getChildren() ] ) {
  877. if ( child.is( 'ul' ) || child.is( 'ol' ) ) {
  878. insertPosition = viewWriter.move( ViewRange.createOn( child ), insertPosition ).end;
  879. mergeViewLists( child, child.nextSibling );
  880. mergeViewLists( child.previousSibling, child );
  881. }
  882. }
  883. }
  884. // Helper function to obtain the first or the last model list item which is in on the same indent level as given `item`.
  885. function _getBoundaryItemOfSameList( item, getFirst ) {
  886. const indent = item.getAttribute( 'indent' );
  887. const direction = getFirst ? 'previousSibling' : 'nextSibling';
  888. let result = item;
  889. while ( item[ direction ] && item[ direction ].is( 'listItem' ) && item[ direction ].getAttribute( 'indent' ) >= indent ) {
  890. item = item[ direction ];
  891. if ( item.getAttribute( 'indent' ) == indent ) {
  892. result = item;
  893. }
  894. }
  895. return result;
  896. }
  897. // Helper function that for given `view.Position`, returns a `view.Position` that is after all `view.UIElement`s that
  898. // are after given position.
  899. // For example:
  900. // <container:p>foo^<ui:span></ui:span><ui:span></ui:span>bar</contain:p>
  901. // For position ^, a position before "bar" will be returned.
  902. function positionAfterUiElements( viewPosition ) {
  903. return viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
  904. }