converters.js 43 KB

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