writer.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890
  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. import isIterable from '../../utils/isiterable.js';
  14. /**
  15. * Tree View Writer class.
  16. * Writer defines a high-level API for TreeView manipulations.
  17. *
  18. * @memberOf engine.treeView
  19. */
  20. export default class Writer {
  21. /**
  22. * Returns first parent container of specified {@link engine.treeView.Position Position}.
  23. * Position's parent node is checked as first, then next parents are checked.
  24. *
  25. * @param {engine.treeView.Position} position Position used as a start point to locate parent container.
  26. * @returns {engine.treeView.Element|undefined} Parent container element or `undefined` if container is not found.
  27. */
  28. getParentContainer( position ) {
  29. let parent = position.parent;
  30. while ( !( parent instanceof ContainerElement ) ) {
  31. if ( !parent ) {
  32. return undefined;
  33. }
  34. parent = parent.parent;
  35. }
  36. return parent;
  37. }
  38. /**
  39. * Breaks attribute nodes at provided position. It breaks `attribute` nodes inside `container` node.
  40. *
  41. * In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes:
  42. *
  43. * <p>{foo}<b><u>{bar}|</u></b></p> -> <p>{foo}<b><u>{bar}</u></b>|</p>
  44. * <p>{foo}<b><u>|{bar}</u></b></p> -> <p>{foo}|<b><u>{bar}</u></b></p>
  45. * <p>{foo}<b><u>{b|ar}</u></b></p> -> <p>{foo}<b><u>{b}</u></b>|<b><u>{ar}</u></b></p>
  46. *
  47. * @see engine.treeView.Writer#isContainer
  48. * @see engine.treeView.Writer#isAttribute
  49. *
  50. * @param {engine.treeView.Position} position Position where to break attributes.
  51. * @returns {engine.treeView.Position} New position after breaking the attributes.
  52. */
  53. breakAttributes( position ) {
  54. const positionOffset = position.offset;
  55. const positionParent = position.parent;
  56. // Position's parent is container, so no attributes to break.
  57. if ( positionParent instanceof ContainerElement ) {
  58. return Position.createFromPosition( position );
  59. }
  60. // Break text and start again in new position.
  61. if ( positionParent instanceof Text ) {
  62. return this.breakAttributes( breakTextNode( position ) );
  63. }
  64. const length = positionParent.getChildCount();
  65. // <p>foo<b><u>bar|</u></b></p>
  66. // <p>foo<b><u>bar</u>|</b></p>
  67. // <p>foo<b><u>bar</u></b>|</p>
  68. if ( positionOffset == length ) {
  69. const newPosition = new Position( positionParent.parent, positionParent.getIndex() + 1 );
  70. return this.breakAttributes( newPosition );
  71. } else
  72. // <p>foo<b><u>|bar</u></b></p>
  73. // <p>foo<b>|<u>bar</u></b></p>
  74. // <p>foo|<b><u>bar</u></b></p>
  75. if ( positionOffset === 0 ) {
  76. const newPosition = new Position( positionParent.parent, positionParent.getIndex() );
  77. return this.breakAttributes( newPosition );
  78. }
  79. // <p>foo<b><u>"b|ar"</u></b></p>
  80. // <p>foo<b><u>"b"|"ar"</u></b></p>
  81. // <p>foo<b><u>b</u>|<u>ar</u></b></p>
  82. // <p>foo<b><u>b</u></b>|<b><u>ar</u></b></p>
  83. else {
  84. const offsetAfter = positionParent.getIndex() + 1;
  85. // Break element.
  86. const clonedNode = positionParent.clone();
  87. // Insert cloned node to position's parent node.
  88. positionParent.parent.insertChildren( offsetAfter, clonedNode );
  89. // Get nodes to move.
  90. const count = positionParent.getChildCount() - positionOffset;
  91. const nodesToMove = positionParent.removeChildren( positionOffset, count );
  92. // Move nodes to cloned node.
  93. clonedNode.appendChildren( nodesToMove );
  94. // Create new position to work on.
  95. const newPosition = new Position( positionParent.parent, offsetAfter );
  96. return this.breakAttributes( newPosition );
  97. }
  98. }
  99. /**
  100. * Uses {@link engine.treeView.Writer#breakAttributes breakAttribute} method to break attributes on
  101. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions of
  102. * provided {@link engine.treeView.Range Range}.
  103. *
  104. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  105. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  106. * same parent container.
  107. *
  108. * @see engine.treeView.Writer#breakAttribute
  109. * @param {engine.treeView.Range} range Range which `start` and `end` positions will be used to break attributes.
  110. * @returns {engine.treeView.Range} New range with located at break positions.
  111. */
  112. breakRange( range ) {
  113. const rangeStart = range.start;
  114. const rangeEnd = range.end;
  115. // Range should be placed inside one container.
  116. if ( this.getParentContainer( rangeStart ) !== this.getParentContainer( rangeEnd ) ) {
  117. /**
  118. * Range is not placed inside same container.
  119. *
  120. * @error treeview-writer-invalid-range-container
  121. */
  122. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  123. }
  124. // Break at the collapsed position. Return new collapsed range.
  125. if ( range.isCollapsed ) {
  126. const position = this.breakAttributes( range.start );
  127. return new Range( position, position );
  128. }
  129. const breakEnd = this.breakAttributes( rangeEnd );
  130. const count = breakEnd.parent.getChildCount();
  131. const breakStart = this.breakAttributes( rangeStart );
  132. // Calculate new break end offset.
  133. breakEnd.offset += breakEnd.parent.getChildCount() - count;
  134. return new Range( breakStart, breakEnd );
  135. }
  136. /**
  137. * Merges attribute nodes. It also merges text nodes if needed.
  138. * Only {@link engine.treeView.AttributeElement#isSimilar similar} `attribute` nodes can be merged.
  139. *
  140. * In following examples `<p>` is a container and `<b>` is an attribute node:
  141. *
  142. * <p>{foo}|{bar}</p> -> <p>{foo|bar}</p>
  143. * <p><b>{foo}</b>|<b>{bar}</b> -> <p><b>{foo|bar}</b></b>
  144. * <p><b foo="bar">{a}</b>|<b foo="baz">{b}</b> -> <p><b foo="bar">{a}</b>|<b foo="baz">{b}</b>
  145. *
  146. * @see engine.treeView.Writer#isContainer
  147. * @see engine.treeView.Writer#isAttribute
  148. * @param {engine.treeView.Position} position Merge position.
  149. * @returns {engine.treeView.Position} Position after merge.
  150. */
  151. mergeAttributes( position ) {
  152. const positionOffset = position.offset;
  153. const positionParent = position.parent;
  154. // When inside text node - nothing to merge.
  155. if ( positionParent instanceof Text ) {
  156. return position;
  157. }
  158. const nodeBefore = positionParent.getChild( positionOffset - 1 );
  159. const nodeAfter = positionParent.getChild( positionOffset );
  160. // Position should be placed between two nodes.
  161. if ( !nodeBefore || !nodeAfter ) {
  162. return position;
  163. }
  164. // When one or both nodes are containers - no attributes to merge.
  165. if ( ( nodeBefore instanceof ContainerElement ) || ( nodeAfter instanceof ContainerElement ) ) {
  166. return position;
  167. }
  168. // When position is between two text nodes.
  169. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  170. return mergeTextNodes( nodeBefore, nodeAfter );
  171. }
  172. // When selection is between same nodes.
  173. else if ( nodeBefore.isSimilar( nodeAfter ) ) {
  174. // Move all children nodes from node placed after selection and remove that node.
  175. const count = nodeBefore.getChildCount();
  176. nodeBefore.appendChildren( nodeAfter.getChildren() );
  177. nodeAfter.remove();
  178. // New position is located inside the first node, before new nodes.
  179. // Call this method recursively to merge again if needed.
  180. return this.mergeAttributes( new Position( nodeBefore, count ) );
  181. }
  182. return position;
  183. }
  184. /**
  185. * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  186. * and merging them afterwards.
  187. *
  188. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-insert-invalid-node` when nodes to insert
  189. * contains instances that are not {@link engine.treeView.Text Texts},
  190. * {@link engine.treeView.AttributeElement AttributeElements} or
  191. * {@link engine.treeView.ContainerElement ContainerElements}.
  192. *
  193. * @param {engine.treeView.Position} position Insertion position.
  194. * @param {engine.treeView.Text|engine.treeView.AttributeElement|engine.treeView.ContainerElement
  195. * |Iterable.<engine.treeView.Text|engine.treeView.AttributeElement|engine.treeView.ContainerElement>} nodes Node or
  196. * nodes to insert.
  197. * @returns {engine.treeView.Range} Range around inserted nodes.
  198. */
  199. insert( position, nodes ) {
  200. nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
  201. // Check if nodes to insert are instances of AttributeElements, ContainerElements or Text.
  202. validateNodesToInsert( nodes );
  203. const container = this.getParentContainer( position );
  204. const insertionPosition = this.breakAttributes( position );
  205. const length = container.insertChildren( insertionPosition.offset, nodes );
  206. const endPosition = insertionPosition.getShiftedBy( length );
  207. const start = this.mergeAttributes( insertionPosition );
  208. // When no nodes were inserted - return collapsed range.
  209. if ( length === 0 ) {
  210. return new Range( start, start );
  211. } else {
  212. // If start position was merged - move end position.
  213. if ( !start.isEqual( insertionPosition ) ) {
  214. endPosition.offset--;
  215. }
  216. const end = this.mergeAttributes( endPosition );
  217. return new Range( start, end );
  218. }
  219. }
  220. /**
  221. * Removes provided range from the container.
  222. *
  223. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  224. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  225. * same parent container.
  226. *
  227. * @param {engine.treeView.Range} range Range to remove from container. After removing, it will be updated
  228. * to a collapsed range showing the new position.
  229. * @returns {engine.treeView.DocumentFragment} Document fragment containing removed nodes.
  230. */
  231. remove( range ) {
  232. // Range should be placed inside one container.
  233. if ( this.getParentContainer( range.start ) !== this.getParentContainer( range.end ) ) {
  234. /**
  235. * Range is not placed inside same container.
  236. *
  237. * @error treeview-writer-invalid-range-container
  238. */
  239. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  240. }
  241. // If range is collapsed - nothing to remove.
  242. if ( range.isCollapsed ) {
  243. return new DocumentFragment();
  244. }
  245. // Break attributes at range start and end.
  246. const { start: breakStart, end: breakEnd } = this.breakRange( range );
  247. const parentContainer = breakStart.parent;
  248. const count = breakEnd.offset - breakStart.offset;
  249. // Remove nodes in range.
  250. const removed = parentContainer.removeChildren( breakStart.offset, count );
  251. // Merge after removing.
  252. const mergePosition = this.mergeAttributes( breakStart );
  253. range.start = mergePosition;
  254. range.end = Position.createFromPosition( mergePosition );
  255. // Return removed nodes.
  256. return new DocumentFragment( removed );
  257. }
  258. /**
  259. * Moves nodes from provided range to target position.
  260. *
  261. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  262. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  263. * same parent container.
  264. *
  265. * @param {engine.treeView.Range} sourceRange Range containing nodes to move.
  266. * @param {engine.treeView.Position} targetPosition Position to insert.
  267. * @returns {engine.treeView.Range} Range in target container. Inserted nodes are placed between
  268. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions.
  269. */
  270. move( sourceRange, targetPosition ) {
  271. const nodes = this.remove( sourceRange );
  272. return this.insert( targetPosition, nodes );
  273. }
  274. /**
  275. * Wraps elements within range with provided {@link engine.treeView.AttributeElement AttributeElement}.
  276. *
  277. * Throws {@link utils.CKEditorError} `treeview-writer-invalid-range-container` when {@link engine.treeView.Range#start}
  278. * and {@link engine.treeView.Range#end} positions are not placed inside same parent container.
  279. * Throws {@link utils.CKEditorError} `treeview-writer-wrap-invalid-attribute` when passed attribute element is not
  280. * an instance of {engine.treeView.AttributeElement AttributeElement}.
  281. *
  282. * @param {engine.treeView.Range} range Range to wrap.
  283. * @param {engine.treeView.AttributeElement} attribute Attribute element to use as wrapper.
  284. */
  285. wrap( range, attribute ) {
  286. if ( !( attribute instanceof AttributeElement ) ) {
  287. /**
  288. * Attribute element need to be instance of attribute element.
  289. *
  290. * @error treeview-writer-wrap-invalid-attribute
  291. */
  292. throw new CKEditorError( 'treeview-writer-wrap-invalid-attribute' );
  293. }
  294. // Range should be placed inside one container.
  295. if ( this.getParentContainer( range.start ) !== this.getParentContainer( range.end ) ) {
  296. /**
  297. * Range is not placed inside same container.
  298. *
  299. * @error treeview-writer-invalid-range-container
  300. */
  301. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  302. }
  303. // If range is collapsed - nothing to wrap.
  304. if ( range.isCollapsed ) {
  305. return range;
  306. }
  307. // Range around one element.
  308. if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) {
  309. const node = range.start.nodeAfter;
  310. if ( node instanceof AttributeElement && wrapAttributeElement( attribute, node ) ) {
  311. return range;
  312. }
  313. }
  314. // Range is inside single attribute and spans on all children.
  315. if ( rangeSpansOnAllChildren( range ) && wrapAttributeElement( attribute, range.start.parent ) ) {
  316. const parent = range.start.parent.parent;
  317. const index = range.start.parent.getIndex();
  318. return Range.createFromParentsAndOffsets( parent, index, parent, index + 1 ) ;
  319. }
  320. // Break attributes at range start and end.
  321. const { start: breakStart, end: breakEnd } = this.breakRange( range );
  322. const parentContainer = breakStart.parent;
  323. // Unwrap children located between break points.
  324. const unwrappedRange = unwrapChildren( this, parentContainer, breakStart.offset, breakEnd.offset, attribute );
  325. // Wrap all children with attribute.
  326. const newRange = wrapChildren( this, parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
  327. // Merge attributes at the both ends and return a new range.
  328. const start = this.mergeAttributes( newRange.start );
  329. // If start position was merged - move end position back.
  330. if ( !start.isEqual( newRange.start ) ) {
  331. newRange.end.offset--;
  332. }
  333. const end = this.mergeAttributes( newRange.end );
  334. return new Range( start, end );
  335. }
  336. /**
  337. * Wraps position with provided attribute. Returns new position after wrapping. This method will also merge newly
  338. * added attribute with its siblings whenever possible.
  339. *
  340. * Throws {@link utils.CKEditorError} `treeview-writer-wrap-invalid-attribute` when passed attribute element is not
  341. * an instance of {engine.treeView.AttributeElement AttributeElement}.
  342. *
  343. * @param {engine.treeView.Position} position
  344. * @param {engine.treeView.AttributeElement} attribute
  345. * @returns {Position} New position after wrapping.
  346. */
  347. wrapPosition( position, attribute ) {
  348. if ( !( attribute instanceof AttributeElement ) ) {
  349. /**
  350. * Attribute element need to be instance of attribute element.
  351. *
  352. * @error treeview-writer-wrap-invalid-attribute
  353. */
  354. throw new CKEditorError( 'treeview-writer-wrap-invalid-attribute' );
  355. }
  356. // Return same position when trying to wrap with attribute similar to position parent.
  357. if ( attribute.isSimilar( position.parent ) ) {
  358. return movePositionToTextNode( Position.createFromPosition( position ) );
  359. }
  360. // When position is inside text node - break it and place new position between two text nodes.
  361. if ( position.parent instanceof Text ) {
  362. position = breakTextNode( position );
  363. }
  364. // Create fake element that will represent position, and will not be merged with other attributes.
  365. const fakePosition = new AttributeElement();
  366. fakePosition.priority = Number.POSITIVE_INFINITY;
  367. fakePosition.isSimilar = () => false;
  368. // Insert fake element in position location.
  369. position.parent.insertChildren( position.offset, fakePosition );
  370. // Range around inserted fake attribute element.
  371. const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
  372. // Wrap fake element with attribute (it will also merge if possible).
  373. this.wrap( wrapRange, attribute );
  374. // Remove fake element and place new position there.
  375. const newPosition = new Position( fakePosition.parent, fakePosition.getIndex() );
  376. fakePosition.remove();
  377. // If position is placed between text nodes - merge them and return position inside.
  378. const nodeBefore = newPosition.nodeBefore;
  379. const nodeAfter = newPosition.nodeAfter;
  380. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  381. return mergeTextNodes( nodeBefore, nodeAfter );
  382. }
  383. // If position is next to text node - move position inside.
  384. return movePositionToTextNode( newPosition );
  385. }
  386. /**
  387. * Unwraps nodes within provided range from attribute element.
  388. *
  389. * Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-invalid-range-container` when
  390. * {@link engine.treeView.Range#start start} and {@link engine.treeView.Range#end end} positions are not placed inside
  391. * same parent container.
  392. *
  393. * @param {engine.treeView.Range} range
  394. * @param {engine.treeView.AttributeElement} element
  395. */
  396. unwrap( range, attribute ) {
  397. if ( !( attribute instanceof AttributeElement ) ) {
  398. /**
  399. * Attribute element need to be instance of attribute element.
  400. *
  401. * @error treeview-writer-unwrap-invalid-attribute
  402. */
  403. throw new CKEditorError( 'treeview-writer-unwrap-invalid-attribute' );
  404. }
  405. // Range should be placed inside one container.
  406. if ( this.getParentContainer( range.start ) !== this.getParentContainer( range.end ) ) {
  407. /**
  408. * Range is not placed inside same container.
  409. *
  410. * @error treeview-writer-invalid-range-container
  411. */
  412. throw new CKEditorError( 'treeview-writer-invalid-range-container' );
  413. }
  414. // If range is collapsed - nothing to unwrap.
  415. if ( range.isCollapsed ) {
  416. return range;
  417. }
  418. // Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
  419. // For example:
  420. // <b class="foo bar" title="baz"></b> unwrap with: <b class="foo"></p> result: <b class"bar" title="baz"></b>
  421. if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) {
  422. const node = range.start.nodeAfter;
  423. // Unwrap single attribute element.
  424. if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && unwrapAttributeElement( attribute, node ) ) {
  425. return range;
  426. }
  427. }
  428. // Break attributes at range start and end.
  429. const { start: breakStart, end: breakEnd } = this.breakRange( range );
  430. const parentContainer = breakStart.parent;
  431. // Unwrap children located between break points.
  432. const newRange = unwrapChildren( this, parentContainer, breakStart.offset, breakEnd.offset, attribute );
  433. // Merge attributes at the both ends and return a new range.
  434. const start = this.mergeAttributes( newRange.start );
  435. // If start position was merged - move end position back.
  436. if ( !start.isEqual( newRange.start ) ) {
  437. newRange.end.offset--;
  438. }
  439. const end = this.mergeAttributes( newRange.end );
  440. return new Range( start, end );
  441. }
  442. }
  443. // Unwraps children from provided `attribute`. Only children contained in `parent` element between
  444. // `startOffset` and `endOffset` will be unwrapped.
  445. //
  446. // @private
  447. // @param {engine.treeView.Writer} writer
  448. // @param {engine.treeView.Element} parent
  449. // @param {Number} startOffset
  450. // @param {Number} endOffset
  451. // @param {engine.treeView.Element} attribute
  452. function unwrapChildren( writer, parent, startOffset, endOffset, attribute ) {
  453. let i = startOffset;
  454. const unwrapPositions = [];
  455. // Iterate over each element between provided offsets inside parent.
  456. while ( i < endOffset ) {
  457. const child = parent.getChild( i );
  458. // If attributes are the similar, then unwrap.
  459. if ( child.isSimilar( attribute ) ) {
  460. const unwrapped = child.getChildren();
  461. const count = child.getChildCount();
  462. // Replace wrapper element with its children
  463. child.remove();
  464. parent.insertChildren( i, unwrapped );
  465. // Save start and end position of moved items.
  466. unwrapPositions.push(
  467. new Position( parent, i ),
  468. new Position( parent, i + count )
  469. );
  470. // Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
  471. // elements.
  472. i += count;
  473. endOffset += count - 1;
  474. } else {
  475. // If other nested attribute is found start unwrapping there.
  476. if ( child instanceof AttributeElement ) {
  477. unwrapChildren( writer, child, 0, child.getChildCount(), attribute );
  478. }
  479. i++;
  480. }
  481. }
  482. // Merge at each unwrap.
  483. let offsetChange = 0;
  484. for ( let position of unwrapPositions ) {
  485. position.offset -= offsetChange;
  486. // Do not merge with elements outside selected children.
  487. if ( position.offset == startOffset || position.offset == endOffset ) {
  488. continue;
  489. }
  490. const newPosition = writer.mergeAttributes( position );
  491. // If nodes were merged - other merge offsets will change.
  492. if ( !newPosition.isEqual( position ) ) {
  493. offsetChange++;
  494. endOffset--;
  495. }
  496. }
  497. return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  498. }
  499. // Wraps children with provided `attribute`. Only children contained in `parent` element between
  500. // `startOffset` and `endOffset` will be wrapped.
  501. // @private
  502. // @param {engine.treeView.Writer} writer
  503. // @param {engine.treeView.Element} parent
  504. // @param {Number} startOffset
  505. // @param {Number} endOffset
  506. // @param {engine.treeView.Element} attribute
  507. function wrapChildren( writer, parent, startOffset, endOffset, attribute ) {
  508. let i = startOffset;
  509. const wrapPositions = [];
  510. while ( i < endOffset ) {
  511. const child = parent.getChild( i );
  512. const isText = child instanceof Text;
  513. const isAttribute = child instanceof AttributeElement;
  514. // Wrap text or attributes with higher or equal priority.
  515. if ( isText || ( isAttribute && attribute.priority <= child.priority ) ) {
  516. // Clone attribute.
  517. const newAttribute = attribute.clone();
  518. // Wrap current node with new attribute;
  519. child.remove();
  520. newAttribute.appendChildren( child );
  521. parent.insertChildren( i, newAttribute );
  522. wrapPositions.push( new Position( parent, i ) );
  523. } else {
  524. // If other nested attribute is found start wrapping there.
  525. if ( child instanceof AttributeElement ) {
  526. wrapChildren( writer, child, 0, child.getChildCount(), attribute );
  527. }
  528. }
  529. i++;
  530. }
  531. // Merge at each wrap.
  532. let offsetChange = 0;
  533. for ( let position of wrapPositions ) {
  534. // Do not merge with elements outside selected children.
  535. if ( position.offset == startOffset ) {
  536. continue;
  537. }
  538. const newPosition = writer.mergeAttributes( position );
  539. // If nodes were merged - other merge offsets will change.
  540. if ( !newPosition.isEqual( position ) ) {
  541. offsetChange++;
  542. endOffset--;
  543. }
  544. }
  545. return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  546. }
  547. // Returns new position that is moved to near text node. Returns same position if there is no text node before of after
  548. // specified position.
  549. //
  550. // <p>{foo}|</p> -> <p>{foo|}</p>
  551. // <p>|{foo}</p> -> <p>{|foo}</p>
  552. //
  553. // @private
  554. // @param {engine.treeView.Position} position
  555. // @returns {engine.treeView.Position} Position located inside text node or same position if there is no text nodes
  556. // before or after position location.
  557. function movePositionToTextNode( position ) {
  558. const nodeBefore = position.nodeBefore;
  559. if ( nodeBefore && nodeBefore instanceof Text ) {
  560. return new Position( nodeBefore, nodeBefore.data.length );
  561. }
  562. const nodeAfter = position.nodeAfter;
  563. if ( nodeAfter && nodeAfter instanceof Text ) {
  564. return new Position( nodeAfter, 0 );
  565. }
  566. return position;
  567. }
  568. // Breaks text node into two text nodes when possible.
  569. //
  570. // <p>{foo|bar}</p> -> <p>{foo}|{bar}</p>
  571. // <p>{|foobar}</p> -> <p>|{foobar}</p>
  572. // <p>{foobar|}</p> -> <p>{foobar}|</p>
  573. //
  574. // @private
  575. // @param {engine.treeView.Position} position Position that need to be placed inside text node.
  576. // @returns {engine.treeView.Position} New position after breaking text node.
  577. function breakTextNode( position ) {
  578. if ( position.offset == position.parent.data.length ) {
  579. return new Position( position.parent.parent, position.parent.getIndex() + 1 );
  580. }
  581. if ( position.offset === 0 ) {
  582. return new Position( position.parent.parent, position.parent.getIndex() );
  583. }
  584. // Get part of the text that need to be moved.
  585. const textToMove = position.parent.data.slice( position.offset );
  586. // Leave rest of the text in position's parent.
  587. position.parent.data = position.parent.data.slice( 0, position.offset );
  588. // Insert new text node after position's parent text node.
  589. position.parent.parent.insertChildren( position.parent.getIndex() + 1, new Text( textToMove ) );
  590. // Return new position between two newly created text nodes.
  591. return new Position( position.parent.parent, position.parent.getIndex() + 1 );
  592. }
  593. // Merges two text nodes into first node. Removes second node and returns merge position.
  594. //
  595. // @private
  596. // @param {engine.treeView.Text} t1 First text node to merge. Data from second text node will be moved at the end of
  597. // this text node.
  598. // @param {engine.treeView.Text} t2 Second text node to merge. This node will be removed after merging.
  599. // @returns {engine.treeView.Position} Position after merging text nodes.
  600. function mergeTextNodes( t1, t2 ) {
  601. // Merge text data into first text node and remove second one.
  602. const nodeBeforeLength = t1.data.length;
  603. t1.data += t2.data;
  604. t2.remove();
  605. return new Position( t1, nodeBeforeLength );
  606. }
  607. // Wraps one {@link engine.treeView.AttributeElement AttributeElement} into another by merging them if possible.
  608. // Two AttributeElements can be merged when there is no attribute or style conflicts between them.
  609. // When merging is possible - all attributes, styles and classes are moved from wrapper element to element being
  610. // wrapped.
  611. //
  612. // @private
  613. // @param {engine.treeView.AttributeElement} wrapper Wrapper AttributeElement.
  614. // @param {engine.treeView.AttributeElement} toWrap AttributeElement to wrap using wrapper element.
  615. // @returns {Boolean} Returns `true` if elements are merged.
  616. function wrapAttributeElement( wrapper, toWrap ) {
  617. // Can't merge if name or priority differs.
  618. if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
  619. return false;
  620. }
  621. // Check if attributes can be merged.
  622. for ( let key of wrapper.getAttributeKeys() ) {
  623. // Classes and styles should be checked separately.
  624. if ( key === 'class' || key === 'style' ) {
  625. continue;
  626. }
  627. // If some attributes are different we cannot wrap.
  628. if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  629. return false;
  630. }
  631. }
  632. // Check if styles can be merged.
  633. for ( let key of wrapper.getStyleNames() ) {
  634. if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  635. return false;
  636. }
  637. }
  638. // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
  639. for ( let key of wrapper.getAttributeKeys() ) {
  640. // Classes and styles should be checked separately.
  641. if ( key === 'class' || key === 'style' ) {
  642. continue;
  643. }
  644. // Move only these attributes that are not present - other are similar.
  645. if ( !toWrap.hasAttribute( key ) ) {
  646. toWrap.setAttribute( key, wrapper.getAttribute( key ) );
  647. }
  648. }
  649. for ( let key of wrapper.getStyleNames() ) {
  650. if ( !toWrap.hasStyle( key ) ) {
  651. toWrap.setStyle( key, wrapper.getStyle( key ) );
  652. }
  653. }
  654. for ( let key of wrapper.getClassNames() ) {
  655. if ( !toWrap.hasClass( key ) ) {
  656. toWrap.addClass( key );
  657. }
  658. }
  659. return true;
  660. }
  661. // Unwraps {@link engine.treeView.AttributeElement AttributeElement} from another by removing corresponding attributes,
  662. // classes and styles. All attributes, classes and styles from wrapper should be present inside element being unwrapped.
  663. //
  664. // @private
  665. // @param {engine.treeView.AttributeElement} wrapper Wrapper AttributeElement.
  666. // @param {engine.treeView.AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
  667. // @returns {Boolean} Returns `true` if elements are unwrapped.
  668. function unwrapAttributeElement( wrapper, toUnwrap ) {
  669. // Can't unwrap if name or priority differs.
  670. if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
  671. return false;
  672. }
  673. // Check if AttributeElement has all wrapper attributes.
  674. for ( let key of wrapper.getAttributeKeys() ) {
  675. // Classes and styles should be checked separately.
  676. if ( key === 'class' || key === 'style' ) {
  677. continue;
  678. }
  679. // If some attributes are missing or different we cannot unwrap.
  680. if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  681. return false;
  682. }
  683. }
  684. // Check if AttributeElement has all wrapper classes.
  685. if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) {
  686. return false;
  687. }
  688. // Check if AttributeElement has all wrapper styles.
  689. for ( let key of wrapper.getStyleNames() ) {
  690. // If some styles are missing or different we cannot unwrap.
  691. if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  692. return false;
  693. }
  694. }
  695. // Remove all wrapper's attributes from unwrapped element.
  696. for ( let key of wrapper.getAttributeKeys() ) {
  697. // Classes and styles should be checked separately.
  698. if ( key === 'class' || key === 'style' ) {
  699. continue;
  700. }
  701. toUnwrap.removeAttribute( key );
  702. }
  703. // Remove all wrapper's classes from unwrapped element.
  704. toUnwrap.removeClass( ...wrapper.getClassNames() );
  705. // Remove all wrapper's styles from unwrapped element.
  706. toUnwrap.removeStyle( ...wrapper.getStyleNames() );
  707. return true;
  708. }
  709. // Returns `true` if range is located in same {@link engine.treeView.AttributeElement AttributeElement}
  710. // (`start` and `end` positions are located inside same {@link engine.treeView.AttributeElement AttributeElement}),
  711. // starts on 0 offset and ends after last child node.
  712. //
  713. // @private
  714. // @param {engine.treeView.Range} Range
  715. // @returns {Boolean}
  716. function rangeSpansOnAllChildren( range ) {
  717. return range.start.parent == range.end.parent && range.start.parent instanceof AttributeElement &&
  718. range.start.offset === 0 && range.end.offset === range.start.parent.getChildCount();
  719. }
  720. // Checks if provided nodes are valid to insert by writer. Checks if each node is an instance of
  721. // {@link engine.treeView.Text Text} or {@link engine.treeView.AttributeElement AttributeElement} or
  722. // {@link engine.treeView.ContainerElement ContainerElement}.
  723. //
  724. // Throws {@link utils.CKEditorError CKEditorError} `treeview-writer-insert-invalid-node` when nodes to insert
  725. // contains instances that are not {@link engine.treeView.Text Texts},
  726. // {@link engine.treeView.AttributeElement AttributeElements} or
  727. // {@link engine.treeView.ContainerElement ContainerElements}.
  728. //
  729. // @private
  730. // @param Iterable.<engine.treeView.Text|engine.treeView.AttributeElement|engine.treeView.ContainerElement> nodes
  731. function validateNodesToInsert( nodes ) {
  732. for ( let node of nodes ) {
  733. if ( !( node instanceof Text || node instanceof AttributeElement || node instanceof ContainerElement ) ) {
  734. /**
  735. * Inserted nodes should be instance of {@link engine.treeView.AttributeElement AttributeElement},
  736. * {@link engine.treeView.ContainerElement ContainerElement} or {@link engine.treeView.Text Text}.
  737. *
  738. * @error treeview-writer-insert-invalid-node
  739. */
  740. throw new CKEditorError( 'treeview-writer-insert-invalid-node' );
  741. }
  742. if ( !( node instanceof Text ) ) {
  743. validateNodesToInsert( node.getChildren() );
  744. }
  745. }
  746. }