8
0

writer.js 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module module:engine/view/writer
  7. */
  8. import Position from './position';
  9. import ContainerElement from './containerelement';
  10. import AttributeElement from './attributeelement';
  11. import EmptyElement from './emptyelement';
  12. import UIElement from './uielement';
  13. import Element from './element';
  14. import Text from './text';
  15. import TextProxy from './textproxy';
  16. import Range from './range';
  17. import TreeWalker from './treewalker';
  18. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  19. import DocumentFragment from './documentfragment';
  20. import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  21. /**
  22. * Contains functions used for composing view tree.
  23. *
  24. * @namespace writer
  25. */
  26. const writer = {
  27. breakAttributes,
  28. breakContainer,
  29. mergeAttributes,
  30. mergeContainers,
  31. insert,
  32. remove,
  33. move,
  34. wrap,
  35. wrapPosition,
  36. unwrap,
  37. rename,
  38. breakViewRangePerContainer
  39. };
  40. export default writer;
  41. /**
  42. * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
  43. * up to a container element.
  44. *
  45. * In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes:
  46. *
  47. * <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
  48. * <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
  49. * <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
  50. * <p><b>fo{o</b><u>ba}r</u></p> -> <p><b>fo</b><b>o</b><u>ba</u><u>r</u></b></p>
  51. *
  52. * **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
  53. *
  54. * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
  55. * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
  56. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first
  57. * encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given
  58. * `position`
  59. * is directly in container element and breaks that container element.
  60. *
  61. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container`
  62. * when {@link module:engine/view/range~Range#start start}
  63. * and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
  64. *
  65. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element`
  66. * when trying to break attributes
  67. * inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
  68. *
  69. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element`
  70. * when trying to break attributes
  71. * inside {@link module:engine/view/uielement~UIElement UIElement}.
  72. *
  73. * @see module:engine/view/attributeelement~AttributeElement
  74. * @see module:engine/view/containerelement~ContainerElement
  75. * @see module:engine/view/writer~writer.breakContainer
  76. * @function module:engine/view/writer~writer.breakAttributes
  77. * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where to break attribute elements.
  78. * @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute
  79. * elements.
  80. */
  81. export function breakAttributes( positionOrRange ) {
  82. if ( positionOrRange instanceof Position ) {
  83. return _breakAttributes( positionOrRange );
  84. } else {
  85. return _breakAttributesRange( positionOrRange );
  86. }
  87. }
  88. /**
  89. * Breaks {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position. Position
  90. * has to be directly inside container element and cannot be in root. Does not break if position is at the beginning
  91. * or at the end of it's parent element.
  92. *
  93. * <p>foo^bar</p> -> <p>foo</p><p>bar</p>
  94. * <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
  95. * <p>^foobar</p> -> ^<p>foobar</p>
  96. * <p>foobar^</p> -> <p>foobar</p>^
  97. *
  98. * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
  99. * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
  100. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first
  101. * encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given
  102. * `position`
  103. * is directly in container element and breaks that container element.
  104. *
  105. * @see module:engine/view/attributeelement~AttributeElement
  106. * @see module:engine/view/containerelement~ContainerElement
  107. * @see module:engine/view/writer~writer.breakAttributes
  108. * @function module:engine/view/writer~writer.breakContainer
  109. * @param {module:engine/view/position~Position} position Position where to break element.
  110. * @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken, the returned position
  111. * is placed either before it or after it.
  112. */
  113. export function breakContainer( position ) {
  114. const element = position.parent;
  115. if ( !( element instanceof ContainerElement ) ) {
  116. /**
  117. * Trying to break an element which is not a container element.
  118. *
  119. * @error view-writer-break-non-container-element
  120. */
  121. throw new CKEditorError( 'view-writer-break-non-container-element: Trying to break an element which is not a container element.' );
  122. }
  123. if ( !element.parent ) {
  124. /**
  125. * Trying to break root element.
  126. *
  127. * @error view-writer-break-root
  128. */
  129. throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
  130. }
  131. if ( position.isAtStart ) {
  132. return Position.createBefore( element );
  133. } else if ( !position.isAtEnd ) {
  134. const newElement = element.clone( false );
  135. insert( Position.createAfter( element ), newElement );
  136. const sourceRange = new Range( position, Position.createAt( element, 'end' ) );
  137. const targetPosition = new Position( newElement, 0 );
  138. move( sourceRange, targetPosition );
  139. }
  140. return Position.createAfter( element );
  141. }
  142. /**
  143. * Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
  144. * Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
  145. *
  146. * In following examples `<p>` is a container and `<b>` is an attribute element:
  147. *
  148. * <p>foo[]bar</p> -> <p>foo{}bar</p>
  149. * <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
  150. * <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
  151. *
  152. * It will also take care about empty attributes when merging:
  153. *
  154. * <p><b>[]</b></p> -> <p>[]</p>
  155. * <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
  156. *
  157. * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
  158. * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
  159. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
  160. * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
  161. *
  162. * @see module:engine/view/attributeelement~AttributeElement
  163. * @see module:engine/view/containerelement~ContainerElement
  164. * @see module:engine/view/writer~writer.mergeContainers
  165. * @function module:engine/view/writer~writer.mergeAttributes
  166. * @param {module:engine/view/position~Position} position Merge position.
  167. * @returns {module:engine/view/position~Position} Position after merge.
  168. */
  169. export function mergeAttributes( position ) {
  170. const positionOffset = position.offset;
  171. const positionParent = position.parent;
  172. // When inside text node - nothing to merge.
  173. if ( positionParent instanceof Text ) {
  174. return position;
  175. }
  176. // When inside empty attribute - remove it.
  177. if ( positionParent instanceof AttributeElement && positionParent.childCount === 0 ) {
  178. const parent = positionParent.parent;
  179. const offset = positionParent.index;
  180. positionParent.remove();
  181. return mergeAttributes( new Position( parent, offset ) );
  182. }
  183. const nodeBefore = positionParent.getChild( positionOffset - 1 );
  184. const nodeAfter = positionParent.getChild( positionOffset );
  185. // Position should be placed between two nodes.
  186. if ( !nodeBefore || !nodeAfter ) {
  187. return position;
  188. }
  189. // When one or both nodes are containers - no attributes to merge.
  190. if ( ( nodeBefore instanceof ContainerElement ) || ( nodeAfter instanceof ContainerElement ) ) {
  191. return position;
  192. }
  193. // When one or both nodes are EmptyElements - no attributes to merge.
  194. if ( ( nodeBefore instanceof EmptyElement ) || ( nodeAfter instanceof EmptyElement ) ) {
  195. return position;
  196. }
  197. // When one or both nodes are UIElements - no attributes to merge.
  198. if ( ( nodeBefore instanceof UIElement ) || ( nodeAfter instanceof UIElement ) ) {
  199. return position;
  200. }
  201. // When position is between two text nodes.
  202. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  203. return mergeTextNodes( nodeBefore, nodeAfter );
  204. }
  205. // When selection is between same nodes.
  206. else if ( nodeBefore.isSimilar( nodeAfter ) ) {
  207. // Move all children nodes from node placed after selection and remove that node.
  208. const count = nodeBefore.childCount;
  209. nodeBefore.appendChildren( nodeAfter.getChildren() );
  210. nodeAfter.remove();
  211. // New position is located inside the first node, before new nodes.
  212. // Call this method recursively to merge again if needed.
  213. return mergeAttributes( new Position( nodeBefore, count ) );
  214. }
  215. return position;
  216. }
  217. /**
  218. * Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
  219. * Precisely, the element after the position is removed and it's contents are moved to element before the position.
  220. *
  221. * <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
  222. * <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
  223. *
  224. * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
  225. * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
  226. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
  227. * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
  228. *
  229. * @see module:engine/view/attributeelement~AttributeElement
  230. * @see module:engine/view/containerelement~ContainerElement
  231. * @see module:engine/view/writer~writer.mergeAttributes
  232. * @function module:engine/view/writer~writer.mergeContainers
  233. * @param {module:engine/view/position~Position} position Merge position.
  234. * @returns {module:engine/view/position~Position} Position after merge.
  235. */
  236. export function mergeContainers( position ) {
  237. const prev = position.nodeBefore;
  238. const next = position.nodeAfter;
  239. if ( !prev || !next || !( prev instanceof ContainerElement ) || !( next instanceof ContainerElement ) ) {
  240. /**
  241. * Element before and after given position cannot be merged.
  242. *
  243. * @error view-writer-merge-containers-invalid-position
  244. */
  245. throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
  246. 'Element before and after given position cannot be merged.' );
  247. }
  248. const lastChild = prev.getChild( prev.childCount - 1 );
  249. const newPosition = lastChild instanceof Text ? Position.createAt( lastChild, 'end' ) : Position.createAt( prev, 'end' );
  250. move( Range.createIn( next ), Position.createAt( prev, 'end' ) );
  251. remove( Range.createOn( next ) );
  252. return newPosition;
  253. }
  254. /**
  255. * Breaks given `range` on a set of {@link module:engine/view/range~Range ranges}, that each are contained within a
  256. * {@link module:engine/view/containerelement~ContainerElement container element}. After `range` is broken, it's "pieces" can
  257. * be used by other {@link module:engine/view/writer~writer} methods (which expect that passed ranges are contained within
  258. * one container element).
  259. *
  260. * @function module:engine/view/writer~writer.breakViewRangePerContainer
  261. * @param {module:engine/view/range~Range} range Range to break.
  262. * @returns {Array.<module:engine/view/range~Range>} Ranges that combine into passed `viewRange`.
  263. */
  264. export function breakViewRangePerContainer( range ) {
  265. const ranges = [];
  266. const walker = new TreeWalker( { boundaries: range } );
  267. let start = range.start;
  268. for ( let value of walker ) {
  269. if ( value.item instanceof ContainerElement ) {
  270. if ( !start.isEqual( value.previousPosition ) ) {
  271. ranges.push( new Range( start, value.previousPosition ) );
  272. }
  273. start = value.nextPosition;
  274. }
  275. }
  276. ranges.push( new Range( start, range.end ) );
  277. return ranges;
  278. }
  279. /**
  280. * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  281. * and merging them afterwards.
  282. *
  283. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
  284. * contains instances that are not {@link module:engine/view/text~Text Texts},
  285. * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
  286. * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
  287. * {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
  288. * {@link module:engine/view/uielement~UIElement UIElements}.
  289. *
  290. * @function insert
  291. * @param {module:engine/view/position~Position} position Insertion position.
  292. * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
  293. * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
  294. * module:engine/view/uielement~UIElement|Iterable.<module:engine/view/text~Text|
  295. * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
  296. * module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
  297. * @returns {module:engine/view/range~Range} Range around inserted nodes.
  298. */
  299. export function insert( position, nodes ) {
  300. nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
  301. // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
  302. validateNodesToInsert( nodes );
  303. const container = getParentContainer( position );
  304. if ( !container ) {
  305. /**
  306. * Position's parent container cannot be found.
  307. *
  308. * @error view-writer-invalid-position-container
  309. */
  310. throw new CKEditorError( 'view-writer-invalid-position-container' );
  311. }
  312. const insertionPosition = _breakAttributes( position, true );
  313. const length = container.insertChildren( insertionPosition.offset, nodes );
  314. const endPosition = insertionPosition.getShiftedBy( length );
  315. const start = mergeAttributes( insertionPosition );
  316. // When no nodes were inserted - return collapsed range.
  317. if ( length === 0 ) {
  318. return new Range( start, start );
  319. } else {
  320. // If start position was merged - move end position.
  321. if ( !start.isEqual( insertionPosition ) ) {
  322. endPosition.offset--;
  323. }
  324. const end = mergeAttributes( endPosition );
  325. return new Range( start, end );
  326. }
  327. }
  328. /**
  329. * Removes provided range from the container.
  330. *
  331. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  332. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  333. * same parent container.
  334. *
  335. * @function module:engine/view/writer~writer.remove
  336. * @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
  337. * to a collapsed range showing the new position.
  338. * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
  339. */
  340. export function remove( range ) {
  341. validateRangeContainer( range );
  342. // If range is collapsed - nothing to remove.
  343. if ( range.isCollapsed ) {
  344. return new DocumentFragment();
  345. }
  346. // Break attributes at range start and end.
  347. const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
  348. const parentContainer = breakStart.parent;
  349. const count = breakEnd.offset - breakStart.offset;
  350. // Remove nodes in range.
  351. const removed = parentContainer.removeChildren( breakStart.offset, count );
  352. // Merge after removing.
  353. const mergePosition = mergeAttributes( breakStart );
  354. range.start = mergePosition;
  355. range.end = Position.createFromPosition( mergePosition );
  356. // Return removed nodes.
  357. return new DocumentFragment( removed );
  358. }
  359. /**
  360. * Removes matching elements from given range.
  361. *
  362. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  363. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  364. * same parent container.
  365. *
  366. * @function module:engine/view/writer~writer.clear
  367. * @param {module:engine/view/range~Range} range Range to clear.
  368. * @param {module:engine/view/element~Element} element Element to remove.
  369. */
  370. export function clear( range, element ) {
  371. validateRangeContainer( range );
  372. // Create walker on given range.
  373. // We walk backward because when we remove element during walk it modify range end position.
  374. const walker = range.getWalker( {
  375. direction: 'backward',
  376. ignoreElementEnd: true
  377. } );
  378. // Let's walk.
  379. for ( const current of walker ) {
  380. const item = current.item;
  381. let rangeToRemove;
  382. // When current item matches to the given element.
  383. if ( item instanceof Element && element.isSimilar( item ) ) {
  384. // Create range on this element.
  385. rangeToRemove = Range.createOn( item );
  386. // When range starts inside Text or TextProxy element.
  387. } else if ( !current.nextPosition.isAfter( range.start ) && ( item instanceof Text || item instanceof TextProxy ) ) {
  388. // We need to check if parent of this text matches to given element.
  389. const parentElement = item.getAncestors().find( ( ancestor ) => {
  390. return ancestor instanceof Element && element.isSimilar( ancestor );
  391. } );
  392. // If it is then create range inside this element.
  393. if ( parentElement && parentElement ) {
  394. rangeToRemove = Range.createIn( parentElement );
  395. }
  396. }
  397. // If we have found element to remove.
  398. if ( rangeToRemove ) {
  399. // We need to check if element range stick out of the given range and truncate if it is.
  400. if ( !range.containsRange( rangeToRemove ) && range.isIntersecting( rangeToRemove ) ) {
  401. if ( rangeToRemove.end.isAfter( range.end ) ) {
  402. rangeToRemove.end = range.end;
  403. }
  404. if ( rangeToRemove.start.isBefore( range.start ) ) {
  405. rangeToRemove.start = range.start;
  406. }
  407. }
  408. // At the end we remove range with founded element.
  409. remove( rangeToRemove );
  410. }
  411. }
  412. }
  413. /**
  414. * Moves nodes from provided range to target position.
  415. *
  416. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  417. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  418. * same parent container.
  419. *
  420. * @function module:engine/view/writer~writer.move
  421. * @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
  422. * @param {module:engine/view/position~Position} targetPosition Position to insert.
  423. * @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
  424. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
  425. */
  426. export function move( sourceRange, targetPosition ) {
  427. let nodes;
  428. if ( targetPosition.isAfter( sourceRange.end ) ) {
  429. targetPosition = _breakAttributes( targetPosition, true );
  430. const parent = targetPosition.parent;
  431. const countBefore = parent.childCount;
  432. sourceRange = _breakAttributesRange( sourceRange, true );
  433. nodes = remove( sourceRange );
  434. targetPosition.offset += ( parent.childCount - countBefore );
  435. } else {
  436. nodes = remove( sourceRange );
  437. }
  438. return insert( targetPosition, nodes );
  439. }
  440. /**
  441. * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
  442. *
  443. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
  444. * when {@link module:engine/view/range~Range#start}
  445. * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
  446. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  447. * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
  448. *
  449. * @function module:engine/view/writer~writer.wrap
  450. * @param {module:engine/view/range~Range} range Range to wrap.
  451. * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
  452. */
  453. export function wrap( range, attribute ) {
  454. if ( !( attribute instanceof AttributeElement ) ) {
  455. /**
  456. * Attribute element need to be instance of attribute element.
  457. *
  458. * @error view-writer-wrap-invalid-attribute
  459. */
  460. throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
  461. }
  462. validateRangeContainer( range );
  463. // If range is collapsed - nothing to wrap.
  464. if ( range.isCollapsed ) {
  465. return range;
  466. }
  467. // Range around one element.
  468. if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) {
  469. const node = range.start.nodeAfter;
  470. if ( node instanceof AttributeElement && wrapAttributeElement( attribute, node ) ) {
  471. return range;
  472. }
  473. }
  474. // Range is inside single attribute and spans on all children.
  475. if ( rangeSpansOnAllChildren( range ) && wrapAttributeElement( attribute, range.start.parent ) ) {
  476. const parent = range.start.parent.parent;
  477. const index = range.start.parent.index;
  478. return Range.createFromParentsAndOffsets( parent, index, parent, index + 1 ) ;
  479. }
  480. // Break attributes at range start and end.
  481. const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
  482. const parentContainer = breakStart.parent;
  483. // Unwrap children located between break points.
  484. const unwrappedRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
  485. // Wrap all children with attribute.
  486. const newRange = wrapChildren( parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
  487. // Merge attributes at the both ends and return a new range.
  488. const start = mergeAttributes( newRange.start );
  489. // If start position was merged - move end position back.
  490. if ( !start.isEqual( newRange.start ) ) {
  491. newRange.end.offset--;
  492. }
  493. const end = mergeAttributes( newRange.end );
  494. return new Range( start, end );
  495. }
  496. /**
  497. * Wraps position with provided attribute. Returns new position after wrapping. This method will also merge newly
  498. * added attribute with its siblings whenever possible.
  499. *
  500. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  501. * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
  502. *
  503. * @param {module:engine/view/position~Position} position
  504. * @param {module:engine/view/attributeelement~AttributeElement} attribute
  505. * @returns {module:engine/view/position~Position} New position after wrapping.
  506. */
  507. export function wrapPosition( position, attribute ) {
  508. if ( !( attribute instanceof AttributeElement ) ) {
  509. /**
  510. * Attribute element need to be instance of attribute element.
  511. *
  512. * @error view-writer-wrap-invalid-attribute
  513. */
  514. throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
  515. }
  516. // Return same position when trying to wrap with attribute similar to position parent.
  517. if ( attribute.isSimilar( position.parent ) ) {
  518. return movePositionToTextNode( Position.createFromPosition( position ) );
  519. }
  520. // When position is inside text node - break it and place new position between two text nodes.
  521. if ( position.parent instanceof Text ) {
  522. position = breakTextNode( position );
  523. }
  524. // Create fake element that will represent position, and will not be merged with other attributes.
  525. const fakePosition = new AttributeElement();
  526. fakePosition.priority = Number.POSITIVE_INFINITY;
  527. fakePosition.isSimilar = () => false;
  528. // Insert fake element in position location.
  529. position.parent.insertChildren( position.offset, fakePosition );
  530. // Range around inserted fake attribute element.
  531. const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
  532. // Wrap fake element with attribute (it will also merge if possible).
  533. wrap( wrapRange, attribute );
  534. // Remove fake element and place new position there.
  535. const newPosition = new Position( fakePosition.parent, fakePosition.index );
  536. fakePosition.remove();
  537. // If position is placed between text nodes - merge them and return position inside.
  538. const nodeBefore = newPosition.nodeBefore;
  539. const nodeAfter = newPosition.nodeAfter;
  540. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  541. return mergeTextNodes( nodeBefore, nodeAfter );
  542. }
  543. // If position is next to text node - move position inside.
  544. return movePositionToTextNode( newPosition );
  545. }
  546. /**
  547. * Unwraps nodes within provided range from attribute element.
  548. *
  549. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  550. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  551. * same parent container.
  552. *
  553. * @param {module:engine/view/range~Range} range
  554. * @param {module:engine/view/attributeelement~AttributeElement} element
  555. */
  556. export function unwrap( range, attribute ) {
  557. if ( !( attribute instanceof AttributeElement ) ) {
  558. /**
  559. * Attribute element need to be instance of attribute element.
  560. *
  561. * @error view-writer-unwrap-invalid-attribute
  562. */
  563. throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' );
  564. }
  565. validateRangeContainer( range );
  566. // If range is collapsed - nothing to unwrap.
  567. if ( range.isCollapsed ) {
  568. return range;
  569. }
  570. // Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
  571. // For example:
  572. // <b class="foo bar" title="baz"></b> unwrap with: <b class="foo"></p> result: <b class"bar" title="baz"></b>
  573. if ( range.end.isEqual( range.start.getShiftedBy( 1 ) ) ) {
  574. const node = range.start.nodeAfter;
  575. // Unwrap single attribute element.
  576. if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && unwrapAttributeElement( attribute, node ) ) {
  577. return range;
  578. }
  579. }
  580. // Break attributes at range start and end.
  581. const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
  582. const parentContainer = breakStart.parent;
  583. // Unwrap children located between break points.
  584. const newRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
  585. // Merge attributes at the both ends and return a new range.
  586. const start = mergeAttributes( newRange.start );
  587. // If start position was merged - move end position back.
  588. if ( !start.isEqual( newRange.start ) ) {
  589. newRange.end.offset--;
  590. }
  591. const end = mergeAttributes( newRange.end );
  592. return new Range( start, end );
  593. }
  594. /**
  595. * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
  596. * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
  597. * has renamed element as {@link module:engine/view/position~Position#parent a parent}.
  598. *
  599. * New element has to be created because `Element#tagName` property in DOM is readonly.
  600. *
  601. * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
  602. *
  603. * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
  604. * @param {String} newName New name for element.
  605. */
  606. export function rename( viewElement, newName ) {
  607. const newElement = new ContainerElement( newName, viewElement.getAttributes() );
  608. insert( Position.createAfter( viewElement ), newElement );
  609. move( Range.createIn( viewElement ), Position.createAt( newElement ) );
  610. remove( Range.createOn( viewElement ) );
  611. return newElement;
  612. }
  613. // Returns first parent container of specified {@link module:engine/view/position~Position Position}.
  614. // Position's parent node is checked as first, then next parents are checked.
  615. // Note that {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
  616. //
  617. // @param {module:engine/view/position~Position} position Position used as a start point to locate parent container.
  618. // @returns {module:engine/view/containerelement~ContainerElement|module:engine/view/documentfragment~DocumentFragment|undefined}
  619. // Parent container element or `undefined` if container is not found.
  620. function getParentContainer( position ) {
  621. let parent = position.parent;
  622. while ( !isContainerOrFragment( parent ) ) {
  623. if ( !parent ) {
  624. return undefined;
  625. }
  626. parent = parent.parent;
  627. }
  628. return parent;
  629. }
  630. // Function used by both public breakAttributes (without splitting text nodes) and by other methods (with
  631. // splitting text nodes).
  632. //
  633. // @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
  634. // @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
  635. // container element. This behavior will result in incorrect view state, but is needed by other view writing methods
  636. // which then fixes view state. Defaults to `false`.
  637. // @returns {module:engine/view/range~Range} New range with located at break positions.
  638. function _breakAttributesRange( range, forceSplitText = false ) {
  639. const rangeStart = range.start;
  640. const rangeEnd = range.end;
  641. validateRangeContainer( range );
  642. // Break at the collapsed position. Return new collapsed range.
  643. if ( range.isCollapsed ) {
  644. const position = _breakAttributes( range.start, forceSplitText );
  645. return new Range( position, position );
  646. }
  647. const breakEnd = _breakAttributes( rangeEnd, forceSplitText );
  648. const count = breakEnd.parent.childCount;
  649. const breakStart = _breakAttributes( rangeStart, forceSplitText );
  650. // Calculate new break end offset.
  651. breakEnd.offset += breakEnd.parent.childCount - count;
  652. return new Range( breakStart, breakEnd );
  653. }
  654. // Function used by public breakAttributes (without splitting text nodes) and by other methods (with
  655. // splitting text nodes).
  656. //
  657. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
  658. // is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
  659. //
  660. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
  661. // is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
  662. //
  663. // @param {module:engine/view/position~Position} position Position where to break attributes.
  664. // @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
  665. // container element. This behavior will result in incorrect view state, but is needed by other view writing methods
  666. // which then fixes view state. Defaults to `false`.
  667. // @returns {module:engine/view/position~Position} New position after breaking the attributes.
  668. function _breakAttributes( position, forceSplitText = false ) {
  669. const positionOffset = position.offset;
  670. const positionParent = position.parent;
  671. // If position is placed inside EmptyElement - throw an exception as we cannot break inside.
  672. if ( position.parent instanceof EmptyElement ) {
  673. /**
  674. * Cannot break inside EmptyElement instance.
  675. *
  676. * @error view-writer-cannot-break-empty-element
  677. */
  678. throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
  679. }
  680. // If position is placed inside UIElement - throw an exception as we cannot break inside.
  681. if ( position.parent instanceof UIElement ) {
  682. /**
  683. * Cannot break inside UIElement instance.
  684. *
  685. * @error view-writer-cannot-break-ui-element
  686. */
  687. throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
  688. }
  689. // There are no attributes to break and text nodes breaking is not forced.
  690. if ( !forceSplitText && positionParent instanceof Text && isContainerOrFragment( positionParent.parent ) ) {
  691. return Position.createFromPosition( position );
  692. }
  693. // Position's parent is container, so no attributes to break.
  694. if ( isContainerOrFragment( positionParent ) ) {
  695. return Position.createFromPosition( position );
  696. }
  697. // Break text and start again in new position.
  698. if ( positionParent instanceof Text ) {
  699. return _breakAttributes( breakTextNode( position ), forceSplitText );
  700. }
  701. const length = positionParent.childCount;
  702. // <p>foo<b><u>bar{}</u></b></p>
  703. // <p>foo<b><u>bar</u>[]</b></p>
  704. // <p>foo<b><u>bar</u></b>[]</p>
  705. if ( positionOffset == length ) {
  706. const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
  707. return _breakAttributes( newPosition, forceSplitText );
  708. } else
  709. // <p>foo<b><u>{}bar</u></b></p>
  710. // <p>foo<b>[]<u>bar</u></b></p>
  711. // <p>foo{}<b><u>bar</u></b></p>
  712. if ( positionOffset === 0 ) {
  713. const newPosition = new Position( positionParent.parent, positionParent.index );
  714. return _breakAttributes( newPosition, forceSplitText );
  715. }
  716. // <p>foo<b><u>b{}ar</u></b></p>
  717. // <p>foo<b><u>b[]ar</u></b></p>
  718. // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
  719. // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
  720. else {
  721. const offsetAfter = positionParent.index + 1;
  722. // Break element.
  723. const clonedNode = positionParent.clone();
  724. // Insert cloned node to position's parent node.
  725. positionParent.parent.insertChildren( offsetAfter, clonedNode );
  726. // Get nodes to move.
  727. const count = positionParent.childCount - positionOffset;
  728. const nodesToMove = positionParent.removeChildren( positionOffset, count );
  729. // Move nodes to cloned node.
  730. clonedNode.appendChildren( nodesToMove );
  731. // Create new position to work on.
  732. const newPosition = new Position( positionParent.parent, offsetAfter );
  733. return _breakAttributes( newPosition, forceSplitText );
  734. }
  735. }
  736. // Unwraps children from provided `attribute`. Only children contained in `parent` element between
  737. // `startOffset` and `endOffset` will be unwrapped.
  738. //
  739. // @param {module:engine/view/element~Element} parent
  740. // @param {Number} startOffset
  741. // @param {Number} endOffset
  742. // @param {module:engine/view/element~Element} attribute
  743. function unwrapChildren( parent, startOffset, endOffset, attribute ) {
  744. let i = startOffset;
  745. const unwrapPositions = [];
  746. // Iterate over each element between provided offsets inside parent.
  747. while ( i < endOffset ) {
  748. const child = parent.getChild( i );
  749. // If attributes are the similar, then unwrap.
  750. if ( child.isSimilar( attribute ) ) {
  751. const unwrapped = child.getChildren();
  752. const count = child.childCount;
  753. // Replace wrapper element with its children
  754. child.remove();
  755. parent.insertChildren( i, unwrapped );
  756. // Save start and end position of moved items.
  757. unwrapPositions.push(
  758. new Position( parent, i ),
  759. new Position( parent, i + count )
  760. );
  761. // Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
  762. // elements.
  763. i += count;
  764. endOffset += count - 1;
  765. } else {
  766. // If other nested attribute is found start unwrapping there.
  767. if ( child instanceof AttributeElement ) {
  768. unwrapChildren( child, 0, child.childCount, attribute );
  769. }
  770. i++;
  771. }
  772. }
  773. // Merge at each unwrap.
  774. let offsetChange = 0;
  775. for ( let position of unwrapPositions ) {
  776. position.offset -= offsetChange;
  777. // Do not merge with elements outside selected children.
  778. if ( position.offset == startOffset || position.offset == endOffset ) {
  779. continue;
  780. }
  781. const newPosition = mergeAttributes( position );
  782. // If nodes were merged - other merge offsets will change.
  783. if ( !newPosition.isEqual( position ) ) {
  784. offsetChange++;
  785. endOffset--;
  786. }
  787. }
  788. return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  789. }
  790. // Wraps children with provided `attribute`. Only children contained in `parent` element between
  791. // `startOffset` and `endOffset` will be wrapped.
  792. //
  793. // @param {module:engine/view/element~Element} parent
  794. // @param {Number} startOffset
  795. // @param {Number} endOffset
  796. // @param {module:engine/view/element~Element} attribute
  797. function wrapChildren( parent, startOffset, endOffset, attribute ) {
  798. let i = startOffset;
  799. const wrapPositions = [];
  800. while ( i < endOffset ) {
  801. const child = parent.getChild( i );
  802. const isText = child instanceof Text;
  803. const isAttribute = child instanceof AttributeElement;
  804. const isEmpty = child instanceof EmptyElement;
  805. const isUI = child instanceof UIElement;
  806. // Wrap text, empty elements, ui elements or attributes with higher or equal priority.
  807. if ( isText || isEmpty || isUI || ( isAttribute && attribute.priority <= child.priority ) ) {
  808. // Clone attribute.
  809. const newAttribute = attribute.clone();
  810. // Wrap current node with new attribute;
  811. child.remove();
  812. newAttribute.appendChildren( child );
  813. parent.insertChildren( i, newAttribute );
  814. wrapPositions.push( new Position( parent, i ) );
  815. } else {
  816. // If other nested attribute is found start wrapping there.
  817. if ( child instanceof AttributeElement ) {
  818. wrapChildren( child, 0, child.childCount, attribute );
  819. }
  820. }
  821. i++;
  822. }
  823. // Merge at each wrap.
  824. let offsetChange = 0;
  825. for ( let position of wrapPositions ) {
  826. position.offset -= offsetChange;
  827. // Do not merge with elements outside selected children.
  828. if ( position.offset == startOffset ) {
  829. continue;
  830. }
  831. const newPosition = mergeAttributes( position );
  832. // If nodes were merged - other merge offsets will change.
  833. if ( !newPosition.isEqual( position ) ) {
  834. offsetChange++;
  835. endOffset--;
  836. }
  837. }
  838. return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  839. }
  840. // Returns new position that is moved to near text node. Returns same position if there is no text node before of after
  841. // specified position.
  842. //
  843. // <p>foo[]</p> -> <p>foo{}</p>
  844. // <p>[]foo</p> -> <p>{}foo</p>
  845. //
  846. // @param {module:engine/view/position~Position} position
  847. // @returns {module:engine/view/position~Position} Position located inside text node or same position if there is no text nodes
  848. // before or after position location.
  849. function movePositionToTextNode( position ) {
  850. const nodeBefore = position.nodeBefore;
  851. if ( nodeBefore && nodeBefore instanceof Text ) {
  852. return new Position( nodeBefore, nodeBefore.data.length );
  853. }
  854. const nodeAfter = position.nodeAfter;
  855. if ( nodeAfter && nodeAfter instanceof Text ) {
  856. return new Position( nodeAfter, 0 );
  857. }
  858. return position;
  859. }
  860. // Breaks text node into two text nodes when possible.
  861. //
  862. // <p>foo{}bar</p> -> <p>foo[]bar</p>
  863. // <p>{}foobar</p> -> <p>[]foobar</p>
  864. // <p>foobar{}</p> -> <p>foobar[]</p>
  865. //
  866. // @param {module:engine/view/position~Position} position Position that need to be placed inside text node.
  867. // @returns {module:engine/view/position~Position} New position after breaking text node.
  868. function breakTextNode( position ) {
  869. if ( position.offset == position.parent.data.length ) {
  870. return new Position( position.parent.parent, position.parent.index + 1 );
  871. }
  872. if ( position.offset === 0 ) {
  873. return new Position( position.parent.parent, position.parent.index );
  874. }
  875. // Get part of the text that need to be moved.
  876. const textToMove = position.parent.data.slice( position.offset );
  877. // Leave rest of the text in position's parent.
  878. position.parent.data = position.parent.data.slice( 0, position.offset );
  879. // Insert new text node after position's parent text node.
  880. position.parent.parent.insertChildren( position.parent.index + 1, new Text( textToMove ) );
  881. // Return new position between two newly created text nodes.
  882. return new Position( position.parent.parent, position.parent.index + 1 );
  883. }
  884. // Merges two text nodes into first node. Removes second node and returns merge position.
  885. //
  886. // @param {module:engine/view/text~Text} t1 First text node to merge. Data from second text node will be moved at the end of
  887. // this text node.
  888. // @param {module:engine/view/text~Text} t2 Second text node to merge. This node will be removed after merging.
  889. // @returns {module:engine/view/position~Position} Position after merging text nodes.
  890. function mergeTextNodes( t1, t2 ) {
  891. // Merge text data into first text node and remove second one.
  892. const nodeBeforeLength = t1.data.length;
  893. t1.data += t2.data;
  894. t2.remove();
  895. return new Position( t1, nodeBeforeLength );
  896. }
  897. // Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by merging them if possible.
  898. // Two AttributeElements can be merged when there is no attribute or style conflicts between them.
  899. // When merging is possible - all attributes, styles and classes are moved from wrapper element to element being
  900. // wrapped.
  901. //
  902. // @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
  903. // @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element.
  904. // @returns {Boolean} Returns `true` if elements are merged.
  905. function wrapAttributeElement( wrapper, toWrap ) {
  906. // Can't merge if name or priority differs.
  907. if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
  908. return false;
  909. }
  910. // Check if attributes can be merged.
  911. for ( let key of wrapper.getAttributeKeys() ) {
  912. // Classes and styles should be checked separately.
  913. if ( key === 'class' || key === 'style' ) {
  914. continue;
  915. }
  916. // If some attributes are different we cannot wrap.
  917. if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  918. return false;
  919. }
  920. }
  921. // Check if styles can be merged.
  922. for ( let key of wrapper.getStyleNames() ) {
  923. if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  924. return false;
  925. }
  926. }
  927. // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
  928. for ( let key of wrapper.getAttributeKeys() ) {
  929. // Classes and styles should be checked separately.
  930. if ( key === 'class' || key === 'style' ) {
  931. continue;
  932. }
  933. // Move only these attributes that are not present - other are similar.
  934. if ( !toWrap.hasAttribute( key ) ) {
  935. toWrap.setAttribute( key, wrapper.getAttribute( key ) );
  936. }
  937. }
  938. for ( let key of wrapper.getStyleNames() ) {
  939. if ( !toWrap.hasStyle( key ) ) {
  940. toWrap.setStyle( key, wrapper.getStyle( key ) );
  941. }
  942. }
  943. for ( let key of wrapper.getClassNames() ) {
  944. if ( !toWrap.hasClass( key ) ) {
  945. toWrap.addClass( key );
  946. }
  947. }
  948. return true;
  949. }
  950. // Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing corresponding attributes,
  951. // classes and styles. All attributes, classes and styles from wrapper should be present inside element being unwrapped.
  952. //
  953. // @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
  954. // @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
  955. // @returns {Boolean} Returns `true` if elements are unwrapped.
  956. function unwrapAttributeElement( wrapper, toUnwrap ) {
  957. // Can't unwrap if name or priority differs.
  958. if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
  959. return false;
  960. }
  961. // Check if AttributeElement has all wrapper attributes.
  962. for ( let key of wrapper.getAttributeKeys() ) {
  963. // Classes and styles should be checked separately.
  964. if ( key === 'class' || key === 'style' ) {
  965. continue;
  966. }
  967. // If some attributes are missing or different we cannot unwrap.
  968. if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  969. return false;
  970. }
  971. }
  972. // Check if AttributeElement has all wrapper classes.
  973. if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) {
  974. return false;
  975. }
  976. // Check if AttributeElement has all wrapper styles.
  977. for ( let key of wrapper.getStyleNames() ) {
  978. // If some styles are missing or different we cannot unwrap.
  979. if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  980. return false;
  981. }
  982. }
  983. // Remove all wrapper's attributes from unwrapped element.
  984. for ( let key of wrapper.getAttributeKeys() ) {
  985. // Classes and styles should be checked separately.
  986. if ( key === 'class' || key === 'style' ) {
  987. continue;
  988. }
  989. toUnwrap.removeAttribute( key );
  990. }
  991. // Remove all wrapper's classes from unwrapped element.
  992. toUnwrap.removeClass( ...wrapper.getClassNames() );
  993. // Remove all wrapper's styles from unwrapped element.
  994. toUnwrap.removeStyle( ...wrapper.getStyleNames() );
  995. return true;
  996. }
  997. // Returns `true` if range is located in same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}
  998. // (`start` and `end` positions are located inside same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}),
  999. // starts on 0 offset and ends after last child node.
  1000. //
  1001. // @param {module:engine/view/range~Range} Range
  1002. // @returns {Boolean}
  1003. function rangeSpansOnAllChildren( range ) {
  1004. return range.start.parent == range.end.parent && range.start.parent instanceof AttributeElement &&
  1005. range.start.offset === 0 && range.end.offset === range.start.parent.childCount;
  1006. }
  1007. // Checks if provided nodes are valid to insert. Checks if each node is an instance of
  1008. // {@link module:engine/view/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
  1009. // {@link module:engine/view/containerelement~ContainerElement ContainerElement},
  1010. // {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or
  1011. // {@link module:engine/view/uielement~UIElement UIElement}.
  1012. //
  1013. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
  1014. // contains instances that are not {@link module:engine/view/text~Text Texts},
  1015. // {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
  1016. // {@link module:engine/view/uielement~UIElement UIElements},
  1017. // {@link module:engine/view/attributeelement~AttributeElement AttributeElements} or
  1018. // {@link module:engine/view/containerelement~ContainerElement ContainerElements}.
  1019. //
  1020. // @param Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement
  1021. // |module:engine/view/containerelement~ContainerElement> nodes
  1022. function validateNodesToInsert( nodes ) {
  1023. for ( let node of nodes ) {
  1024. if (
  1025. !( node instanceof Text || node instanceof AttributeElement || node instanceof ContainerElement ||
  1026. node instanceof EmptyElement || node instanceof UIElement )
  1027. ) {
  1028. /**
  1029. * Inserted nodes should be instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
  1030. * {@link module:engine/view/containerelement~ContainerElement ContainerElement},
  1031. * {@link module:engine/view/emptyerelement~EmptyElement EmptyElement},
  1032. * {@link module:engine/view/uierelement~UIElement UIElement}, {@link module:engine/view/text~Text Text}.
  1033. *
  1034. * @error view-writer-insert-invalid-node
  1035. */
  1036. throw new CKEditorError( 'view-writer-insert-invalid-node' );
  1037. }
  1038. if ( !( node instanceof Text ) ) {
  1039. validateNodesToInsert( node.getChildren() );
  1040. }
  1041. }
  1042. }
  1043. // Checks if node is ContainerElement or DocumentFragment, because in most cases they should be treated the same way.
  1044. //
  1045. // @param {module:engine/view/node~Node} node
  1046. // @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment.
  1047. function isContainerOrFragment( node ) {
  1048. return node instanceof ContainerElement || node instanceof DocumentFragment;
  1049. }
  1050. // Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed
  1051. // inside same {@link module:engine/view/containerelement~ContainerElement container element}.
  1052. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
  1053. //
  1054. // @param {module:engine/view/range~Range} range
  1055. function validateRangeContainer( range ) {
  1056. const startContainer = getParentContainer( range.start );
  1057. const endContainer = getParentContainer( range.end );
  1058. if ( !startContainer || !endContainer || startContainer !== endContainer ) {
  1059. /**
  1060. * Range container is invalid. This can happen if {@link module:engine/view/range~Range#start range start} and
  1061. * {@link module:engine/view/range~Range#end range end} positions are not placed inside same container or
  1062. * parent container for these positions cannot be found.
  1063. *
  1064. * @error view-writer-invalid-range-container
  1065. */
  1066. throw new CKEditorError( 'view-writer-invalid-range-container' );
  1067. }
  1068. }