writer.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. 'use strict';
  6. import Position from './position.js';
  7. import ContainerElement from './containerelement.js';
  8. import AttributeElement from './attributeelement.js';
  9. import Text from './text.js';
  10. import Range from './range.js';
  11. import CKEditorError from '../../utils/ckeditorerror.js';
  12. import DocumentFragment from './documentfragment.js';
  13. /**
  14. * Tree View Writer class.
  15. * Writer defines a high-level API for TreeView manipulations.
  16. *
  17. * @memberOf engine.treeView
  18. */
  19. export default class Writer {
  20. /**
  21. * Returns first parent container of specified {@link engine.treeView.Position Position}.
  22. * Position's parent node is checked as first, then next parents are checked.
  23. *
  24. * @param {engine.treeView.Position} position Position used as a start point to locate parent container.
  25. * @returns {engine.treeView.Element|undefined} Parent container element or `undefined` if container is not found.
  26. */
  27. getParentContainer( position ) {
  28. let parent = position.parent;
  29. while ( !( parent instanceof ContainerElement ) ) {
  30. if ( !parent ) {
  31. return undefined;
  32. }
  33. parent = parent.parent;
  34. }
  35. return parent;
  36. }
  37. /**
  38. * Breaks attribute nodes at provided position. It breaks `attribute` nodes inside `container` node.
  39. *
  40. * In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes:
  41. *
  42. * <p>{foo}<b><u>{bar}|</u></b></p> -> <p>{foo}<b><u>{bar}</u></b>|</p>
  43. * <p>{foo}<b><u>|{bar}</u></b></p> -> <p>{foo}|<b><u>{bar}</u></b></p>
  44. * <p>{foo}<b><u>{b|ar}</u></b></p> -> <p>{foo}<b><u>{b}</u></b>|<b><u>{ar}</u></b></p>
  45. *
  46. * @see engine.treeView.Writer#isContainer
  47. * @see engine.treeView.Writer#isAttribute
  48. *
  49. * @param {engine.treeView.Position} position Position where to break attributes.
  50. * @returns {engine.treeView.Position} New position after breaking the attributes.
  51. */
  52. breakAttributes( position ) {
  53. const positionOffset = position.offset;
  54. const positionParent = position.parent;
  55. // Position's parent is container, so no attributes to break.
  56. if ( positionParent instanceof ContainerElement ) {
  57. return Position.createFromPosition( position );
  58. }
  59. // Break text and start again in new position.
  60. if ( positionParent instanceof Text ) {
  61. return this.breakAttributes( breakTextNode( position ) );
  62. }
  63. const length = positionParent.getChildCount();
  64. // <p>foo<b><u>bar|</u></b></p>
  65. // <p>foo<b><u>bar</u>|</b></p>
  66. // <p>foo<b><u>bar</u></b>|</p>
  67. if ( positionOffset == length ) {
  68. const newPosition = new Position( positionParent.parent, positionParent.getIndex() + 1 );
  69. return this.breakAttributes( newPosition );
  70. } else
  71. // <p>foo<b><u>|bar</u></b></p>
  72. // <p>foo<b>|<u>bar</u></b></p>
  73. // <p>foo|<b><u>bar</u></b></p>
  74. if ( positionOffset === 0 ) {
  75. const newPosition = new Position( positionParent.parent, positionParent.getIndex() );
  76. return this.breakAttributes( newPosition );
  77. }
  78. // <p>foo<b><u>"b|ar"</u></b></p>
  79. // <p>foo<b><u>"b"|"ar"</u></b></p>
  80. // <p>foo<b><u>b</u>|<u>ar</u></b></p>
  81. // <p>foo<b><u>b</u></b>|<b><u>ar</u></b></p>
  82. else {
  83. const offsetAfter = positionParent.getIndex() + 1;
  84. // Break element.
  85. const clonedNode = positionParent.clone();
  86. // Insert cloned node to position's parent node.
  87. positionParent.parent.insertChildren( offsetAfter, clonedNode );
  88. // Get nodes to move.
  89. const count = positionParent.getChildCount() - positionOffset;
  90. const nodesToMove = positionParent.removeChildren( positionOffset, count );
  91. // Move nodes to cloned node.
  92. clonedNode.appendChildren( nodesToMove );
  93. // Create new position to work on.
  94. const newPosition = new Position( positionParent.parent, offsetAfter );
  95. return this.breakAttributes( newPosition );
  96. }
  97. }
  98. /**
  99. * Uses {@link engine.treeView.Writer#breakAttributes breakAttribute} method to break attributes on
  100. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions of
  101. * provided {@link engine.treeView.Range Range}.
  102. *
  103. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  104. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  105. * same parent container.
  106. *
  107. * @see engine.treeView.Writer#breakAttribute
  108. * @param {engine.treeView.Range} range Range which `start` and `end` positions will be used to break attributes.
  109. * @returns {engine.treeView.Range} New range with located at break positions.
  110. */
  111. breakRange( range ) {
  112. const rangeStart = range.start;
  113. const rangeEnd = range.end;
  114. // Range should be placed inside one container.
  115. if ( this.getParentContainer( rangeStart ) !== this.getParentContainer( rangeEnd ) ) {
  116. /**
  117. * Range is not placed inside same container.
  118. *
  119. * @error treeview-writer-invalid-range-container
  120. */
  121. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  122. }
  123. // Break at the collapsed position. Return new collapsed range.
  124. if ( range.isCollapsed ) {
  125. const position = this.breakAttributes( range.start );
  126. return new Range( position, position );
  127. }
  128. const breakEnd = this.breakAttributes( rangeEnd );
  129. const count = breakEnd.parent.getChildCount();
  130. const breakStart = this.breakAttributes( rangeStart );
  131. // Calculate new break end offset.
  132. breakEnd.offset += breakEnd.parent.getChildCount() - count;
  133. return new Range( breakStart, breakEnd );
  134. }
  135. /**
  136. * Merges attribute nodes. It also merges text nodes if needed.
  137. * Only {@link engine.treeView.AttributeElement#isSimilar similar} `attribute` nodes can be merged.
  138. *
  139. * In following examples `<p>` is a container and `<b>` is an attribute node:
  140. *
  141. * <p>{foo}|{bar}</p> -> <p>{foo|bar}</p>
  142. * <p><b>{foo}</b>|<b>{bar}</b> -> <p><b>{foo|bar}</b></b>
  143. * <p><b foo="bar">{a}</b>|<b foo="baz">{b}</b> -> <p><b foo="bar">{a}</b>|<b foo="baz">{b}</b>
  144. *
  145. * @see engine.treeView.Writer#isContainer
  146. * @see engine.treeView.Writer#isAttribute
  147. * @param {engine.treeView.Position} position Merge position.
  148. * @returns {engine.treeView.Position} Position after merge.
  149. */
  150. mergeAttributes( position ) {
  151. const positionOffset = position.offset;
  152. const positionParent = position.parent;
  153. // When inside text node - nothing to merge.
  154. if ( positionParent instanceof Text ) {
  155. return position;
  156. }
  157. const nodeBefore = positionParent.getChild( positionOffset - 1 );
  158. const nodeAfter = positionParent.getChild( positionOffset );
  159. // Position should be placed between two nodes.
  160. if ( !nodeBefore || !nodeAfter ) {
  161. return position;
  162. }
  163. // When one or both nodes are containers - no attributes to merge.
  164. if ( ( nodeBefore instanceof ContainerElement ) || ( nodeAfter instanceof ContainerElement ) ) {
  165. return position;
  166. }
  167. // When position is between two text nodes.
  168. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  169. return mergeTextNodes( nodeBefore, nodeAfter );
  170. }
  171. // When selection is between same nodes.
  172. else if ( nodeBefore.isSimilar( nodeAfter ) ) {
  173. // Move all children nodes from node placed after selection and remove that node.
  174. const count = nodeBefore.getChildCount();
  175. nodeBefore.appendChildren( nodeAfter.getChildren() );
  176. nodeAfter.remove();
  177. // New position is located inside the first node, before new nodes.
  178. // Call this method recursively to merge again if needed.
  179. return this.mergeAttributes( new Position( nodeBefore, count ) );
  180. }
  181. return position;
  182. }
  183. /**
  184. * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  185. * and merging them afterwards.
  186. *
  187. * @param {engine.treeView.Position} position Insertion position.
  188. * @param {engine.treeView.Node|Iterable.<engine.treeView.Node>} nodes Node or nodes to insert.
  189. * @returns {engine.treeView.Range} Range around inserted nodes.
  190. */
  191. insert( position, nodes ) {
  192. const container = this.getParentContainer( position );
  193. const insertionPosition = this.breakAttributes( position );
  194. const length = container.insertChildren( insertionPosition.offset, nodes );
  195. const endPosition = insertionPosition.getShiftedBy( length );
  196. const start = this.mergeAttributes( insertionPosition );
  197. // When no nodes were inserted - return collapsed range.
  198. if ( length === 0 ) {
  199. return new Range( start, start );
  200. } else {
  201. // If start position was merged - move end position.
  202. if ( !start.isEqual( insertionPosition ) ) {
  203. endPosition.offset--;
  204. }
  205. const end = this.mergeAttributes( endPosition );
  206. return new Range( start, end );
  207. }
  208. }
  209. /**
  210. * Removes provided range from the container.
  211. *
  212. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  213. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  214. * same parent container.
  215. *
  216. * @param {engine.treeView.Range} range Range to remove from container. After removing, it will be updated
  217. * to a collapsed range showing the new position.
  218. * @returns {engine.treeView.DocumentFragment} Document fragment containing removed nodes.
  219. */
  220. remove( range ) {
  221. // Range should be placed inside one container.
  222. if ( this.getParentContainer( range.start ) !== this.getParentContainer( range.end ) ) {
  223. /**
  224. * Range is not placed inside same container.
  225. *
  226. * @error treeview-writer-invalid-range-container
  227. */
  228. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  229. }
  230. // If range is collapsed - nothing to remove.
  231. if ( range.isCollapsed ) {
  232. return new DocumentFragment();
  233. }
  234. // Break attributes at range start and end.
  235. const { start: breakStart, end: breakEnd } = this.breakRange( range );
  236. const parentContainer = breakStart.parent;
  237. const count = breakEnd.offset - breakStart.offset;
  238. // Remove nodes in range.
  239. const removed = parentContainer.removeChildren( breakStart.offset, count );
  240. // Merge after removing.
  241. const mergePosition = this.mergeAttributes( breakStart );
  242. range.start = mergePosition;
  243. range.end = Position.createFromPosition( mergePosition );
  244. // Return removed nodes.
  245. return new DocumentFragment( removed );
  246. }
  247. /**
  248. * Moves nodes from provided range to target position.
  249. *
  250. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  251. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  252. * same parent container.
  253. *
  254. * @param {engine.treeView.Range} sourceRange Range containing nodes to move.
  255. * @param {engine.treeView.Position} targetPosition Position to insert.
  256. * @returns {engine.treeView.Range} Range in target container. Inserted nodes are placed between
  257. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions.
  258. */
  259. move( sourceRange, targetPosition ) {
  260. const nodes = this.remove( sourceRange );
  261. return this.insert( targetPosition, nodes );
  262. }
  263. /**
  264. * Wraps elements within range with provided attribute element.
  265. *
  266. * Throws {@link utils.CKEditorError} `treeview-writer-invalid-range-container` when {@link engine.treeView.Range#start}
  267. * and {@link engine.treeView.Range#end} positions are not placed inside same parent container.
  268. * Throws {@link utils.CKEditorError} `treeview-writer-wrap-invalid-attribute` when passed attribute element is not
  269. * an instance of {engine.treeView.AttributeElement AttributeElement}.
  270. *
  271. * @param {engine.treeView.Range} range Range to wrap.
  272. * @param {engine.treeView.AttributeElement} attribute Attribute element to use as wrapper.
  273. */
  274. wrap( range, attribute ) {
  275. if ( !( attribute instanceof AttributeElement ) ) {
  276. /**
  277. * Attribute element need to be instance of attribute element.
  278. *
  279. * @error treeview-writer-wrap-invalid-attribute
  280. */
  281. throw new CKEditorError( 'treeview-writer-wrap-invalid-attribute' );
  282. }
  283. // Range should be placed inside one container.
  284. if ( this.getParentContainer( range.start ) !== this.getParentContainer( range.end ) ) {
  285. /**
  286. * Range is not placed inside same container.
  287. *
  288. * @error treeview-writer-invalid-range-container
  289. */
  290. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  291. }
  292. // If range is collapsed - nothing to wrap.
  293. if ( range.isCollapsed ) {
  294. return range;
  295. }
  296. // Break attributes at range start and end.
  297. const { start: breakStart, end: breakEnd } = this.breakRange( range );
  298. const parentContainer = breakStart.parent;
  299. // Unwrap children located between break points.
  300. const unwrappedRange = unwrapChildren( this, parentContainer, breakStart.offset, breakEnd.offset, attribute );
  301. // Wrap all children with attribute.
  302. const newRange = wrapChildren( this, parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
  303. // Merge attributes at the both ends and return a new range.
  304. const start = this.mergeAttributes( newRange.start );
  305. // If start position was merged - move end position back.
  306. if ( !start.isEqual( newRange.start ) ) {
  307. newRange.end.offset--;
  308. }
  309. const end = this.mergeAttributes( newRange.end );
  310. return new Range( start, end );
  311. }
  312. /**
  313. * Wraps position with provided attribute. Returns new position after wrapping. This method will also merge newly
  314. * added attribute with its siblings whenever possible.
  315. *
  316. * Throws {@link utils.CKEditorError} `treeview-writer-wrap-invalid-attribute` when passed attribute element is not
  317. * an instance of {engine.treeView.AttributeElement AttributeElement}.
  318. *
  319. * @param {engine.treeView.Position} position
  320. * @param {engine.treeView.AttributeElement} attribute
  321. * @returns {Position} New position after wrapping.
  322. */
  323. wrapPosition( position, attribute ) {
  324. if ( !( attribute instanceof AttributeElement ) ) {
  325. /**
  326. * Attribute element need to be instance of attribute element.
  327. *
  328. * @error treeview-writer-wrap-invalid-attribute
  329. */
  330. throw new CKEditorError( 'treeview-writer-wrap-invalid-attribute' );
  331. }
  332. // Return same position when trying to wrap with attribute similar to position parent.
  333. if ( attribute.isSimilar( position.parent ) ) {
  334. return movePositionToTextNode( Position.createFromPosition( position ) );
  335. }
  336. // When position is inside text node - break it and place new position between two text nodes.
  337. if ( position.parent instanceof Text ) {
  338. position = breakTextNode( position );
  339. }
  340. // Create fake element that will represent position, and will not be merged with other attributes.
  341. const fakePosition = new AttributeElement();
  342. fakePosition.priority = Number.POSITIVE_INFINITY;
  343. fakePosition.isSimilar = () => false;
  344. // Insert fake element in position location.
  345. position.parent.insertChildren( position.offset, fakePosition );
  346. // Range around inserted fake attribute element.
  347. const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
  348. // Wrap fake element with attribute (it will also merge if possible).
  349. this.wrap( wrapRange, attribute );
  350. // Remove fake element and place new position there.
  351. const newPosition = new Position( fakePosition.parent, fakePosition.getIndex() );
  352. fakePosition.remove();
  353. // If position is placed between text nodes - merge them and return position inside.
  354. const nodeBefore = newPosition.nodeBefore;
  355. const nodeAfter = newPosition.nodeAfter;
  356. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  357. return mergeTextNodes( nodeBefore, nodeAfter );
  358. }
  359. // If position is next to text node - move position inside.
  360. return movePositionToTextNode( newPosition );
  361. }
  362. /**
  363. * Unwraps nodes within provided range from attribute element.
  364. *
  365. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  366. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  367. * same parent container.
  368. *
  369. * @param {engine.treeView.Range} range
  370. * @param {engine.treeView.AttributeElement} element
  371. */
  372. unwrap( range, attribute ) {
  373. if ( !( attribute instanceof AttributeElement ) ) {
  374. /**
  375. * Attribute element need to be instance of attribute element.
  376. *
  377. * @error treeview-writer-unwrap-invalid-attribute
  378. */
  379. throw new CKEditorError( 'treeview-writer-unwrap-invalid-attribute' );
  380. }
  381. // Range should be placed inside one container.
  382. if ( this.getParentContainer( range.start ) !== this.getParentContainer( range.end ) ) {
  383. /**
  384. * Range is not placed inside same container.
  385. *
  386. * @error treeview-writer-invalid-range-container
  387. */
  388. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  389. }
  390. // If range is collapsed - nothing to unwrap.
  391. if ( range.isCollapsed ) {
  392. return range;
  393. }
  394. // Break attributes at range start and end.
  395. const { start: breakStart, end: breakEnd } = this.breakRange( range );
  396. const parentContainer = breakStart.parent;
  397. // Unwrap children located between break points.
  398. const newRange = unwrapChildren( this, parentContainer, breakStart.offset, breakEnd.offset, attribute );
  399. // Merge attributes at the both ends and return a new range.
  400. const start = this.mergeAttributes( newRange.start );
  401. // If start position was merged - move end position back.
  402. if ( !start.isEqual( newRange.start ) ) {
  403. newRange.end.offset--;
  404. }
  405. const end = this.mergeAttributes( newRange.end );
  406. return new Range( start, end );
  407. }
  408. }
  409. // Unwraps children from provided `attribute`. Only children contained in `parent` element between
  410. // `startOffset` and `endOffset` will be unwrapped.
  411. //
  412. // @private
  413. // @param {engine.treeView.Writer} writer
  414. // @param {engine.treeView.Element} parent
  415. // @param {Number} startOffset
  416. // @param {Number} endOffset
  417. // @param {engine.treeView.Element} attribute
  418. function unwrapChildren( writer, parent, startOffset, endOffset, attribute ) {
  419. let i = startOffset;
  420. const unwrapPositions = [];
  421. // Iterate over each element between provided offsets inside parent.
  422. while ( i < endOffset ) {
  423. const child = parent.getChild( i );
  424. // If attributes are the similar, then unwrap.
  425. if ( child.isSimilar( attribute ) ) {
  426. const unwrapped = child.getChildren();
  427. const count = child.getChildCount();
  428. // Replace wrapper element with its children
  429. child.remove();
  430. parent.insertChildren( i, unwrapped );
  431. // Save start and end position of moved items.
  432. unwrapPositions.push(
  433. new Position( parent, i ),
  434. new Position( parent, i + count )
  435. );
  436. // Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
  437. // elements.
  438. i += count;
  439. endOffset += count - 1;
  440. } else {
  441. // If other nested attribute is found start unwrapping there.
  442. if ( child instanceof AttributeElement ) {
  443. unwrapChildren( writer, child, 0, child.getChildCount(), attribute );
  444. }
  445. i++;
  446. }
  447. }
  448. // Merge at each unwrap.
  449. let offsetChange = 0;
  450. for ( let position of unwrapPositions ) {
  451. position.offset -= offsetChange;
  452. // Do not merge with elements outside selected children.
  453. if ( position.offset == startOffset || position.offset == endOffset ) {
  454. continue;
  455. }
  456. const newPosition = writer.mergeAttributes( position );
  457. // If nodes were merged - other merge offsets will change.
  458. if ( !newPosition.isEqual( position ) ) {
  459. offsetChange++;
  460. endOffset--;
  461. }
  462. }
  463. return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  464. }
  465. // Wraps children with provided `attribute`. Only children contained in `parent` element between
  466. // `startOffset` and `endOffset` will be wrapped.
  467. // @private
  468. // @param {engine.treeView.Writer} writer
  469. // @param {engine.treeView.Element} parent
  470. // @param {Number} startOffset
  471. // @param {Number} endOffset
  472. // @param {engine.treeView.Element} attribute
  473. function wrapChildren( writer, parent, startOffset, endOffset, attribute ) {
  474. let i = startOffset;
  475. const wrapPositions = [];
  476. while ( i < endOffset ) {
  477. const child = parent.getChild( i );
  478. const isText = child instanceof Text;
  479. const isAttribute = child instanceof AttributeElement;
  480. // Wrap text or attributes with higher or equal priority.
  481. if ( isText || ( isAttribute && attribute.priority <= child.priority ) ) {
  482. // Clone attribute.
  483. const newAttribute = attribute.clone();
  484. // Wrap current node with new attribute;
  485. child.remove();
  486. newAttribute.appendChildren( child );
  487. parent.insertChildren( i, newAttribute );
  488. wrapPositions.push( new Position( parent, i ) );
  489. } else {
  490. // If other nested attribute is found start wrapping there.
  491. if ( child instanceof AttributeElement ) {
  492. wrapChildren( writer, child, 0, child.getChildCount(), attribute );
  493. }
  494. }
  495. i++;
  496. }
  497. // Merge at each wrap.
  498. let offsetChange = 0;
  499. for ( let position of wrapPositions ) {
  500. // Do not merge with elements outside selected children.
  501. if ( position.offset == startOffset ) {
  502. continue;
  503. }
  504. const newPosition = writer.mergeAttributes( position );
  505. // If nodes were merged - other merge offsets will change.
  506. if ( !newPosition.isEqual( position ) ) {
  507. offsetChange++;
  508. endOffset--;
  509. }
  510. }
  511. return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  512. }
  513. // Returns new position that is moved to near text node. Returns same position if there is no text node before of after
  514. // specified position.
  515. //
  516. // <p>{foo}|</p> -> <p>{foo|}</p>
  517. // <p>|{foo}</p> -> <p>{|foo}</p>
  518. //
  519. // @param {engine.treeView.Position} position
  520. // @returns {engine.treeView.Position} Position located inside text node or same position if there is no text nodes
  521. // before or after position location.
  522. function movePositionToTextNode( position ) {
  523. const nodeBefore = position.nodeBefore;
  524. if ( nodeBefore && nodeBefore instanceof Text ) {
  525. return new Position( nodeBefore, nodeBefore.data.length );
  526. }
  527. const nodeAfter = position.nodeAfter;
  528. if ( nodeAfter && nodeAfter instanceof Text ) {
  529. return new Position( nodeAfter, 0 );
  530. }
  531. return position;
  532. }
  533. // Breaks text node into two text nodes when possible.
  534. //
  535. // <p>{foo|bar}</p> -> <p>{foo}|{bar}</p>
  536. // <p>{|foobar}</p> -> <p>|{foobar}</p>
  537. // <p>{foobar|}</p> -> <p>{foobar}|</p>
  538. //
  539. // @param {engine.treeView.Position} position Position that need to be placed inside text node.
  540. // @returns {engine.treeView.Position} New position after breaking text node.
  541. function breakTextNode( position ) {
  542. if ( position.offset == position.parent.data.length ) {
  543. return new Position( position.parent.parent, position.parent.getIndex() + 1 );
  544. }
  545. if ( position.offset === 0 ) {
  546. return new Position( position.parent.parent, position.parent.getIndex() );
  547. }
  548. // Get part of the text that need to be moved.
  549. const textToMove = position.parent.data.slice( position.offset );
  550. // Leave rest of the text in position's parent.
  551. position.parent.data = position.parent.data.slice( 0, position.offset );
  552. // Insert new text node after position's parent text node.
  553. position.parent.parent.insertChildren( position.parent.getIndex() + 1, new Text( textToMove ) );
  554. // Return new position between two newly created text nodes.
  555. return new Position( position.parent.parent, position.parent.getIndex() + 1 );
  556. }
  557. // Merges two text nodes into first node. Removes second node and returns merge position.
  558. //
  559. // @param {engine.treeView.Text} t1 First text node to merge. Data from second text node will be moved at the end of
  560. // this text node.
  561. // @param {engine.treeView.Text} t2 Second text node to merge. This node will be removed after merging.
  562. // @returns {engine.treeView.Position} Position after merging text nodes.
  563. function mergeTextNodes( t1, t2 ) {
  564. // Merge text data into first text node and remove second one.
  565. const nodeBeforeLength = t1.data.length;
  566. t1.data += t2.data;
  567. t2.remove();
  568. return new Position( t1, nodeBeforeLength );
  569. }