writer.js 33 KB

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