writer.js 34 KB

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