writer.js 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594
  1. /**
  2. * @license Copyright (c) 2003-2018, 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 Range from './range';
  14. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  15. import DocumentFragment from './documentfragment';
  16. import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  17. import isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject';
  18. import Text from './text';
  19. import EditableElement from './editableelement';
  20. /**
  21. * View writer class. Provides set of methods used to properly manipulate nodes attached to
  22. * {@link module:engine/view/document~Document view document}. It is not recommended to use it directly. To get an instance
  23. * of view writer associated with the document use {@link module:engine/view/view~View#change view.change()) method.
  24. */
  25. export default class Writer {
  26. constructor( document ) {
  27. /**
  28. * @readonly
  29. * @type {module:engine/view/document~Document}
  30. */
  31. this.document = document;
  32. }
  33. /**
  34. * Sets {@link module:engine/view/selection~Selection selection's} ranges and direction to the specified location based on the given
  35. * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
  36. * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
  37. * an iterable of {@link module:engine/view/range~Range ranges} or null.
  38. *
  39. * ### Usage:
  40. *
  41. * // Sets selection to the given range.
  42. * const range = new Range( start, end );
  43. * writer.setSelection( range );
  44. *
  45. * // Sets backward selection to the given range.
  46. * const range = new Range( start, end );
  47. * writer.setSelection( range );
  48. *
  49. * // Sets selection to given ranges.
  50. * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
  51. * writer.setSelection( range );
  52. *
  53. * // Sets selection to the other selection.
  54. * const otherSelection = new Selection();
  55. * writer.setSelection( otherSelection );
  56. *
  57. * // Sets collapsed selection at the given position.
  58. * const position = new Position( root, path );
  59. * writer.setSelection( position );
  60. *
  61. * // Sets collapsed selection at the position of given item and offset.
  62. * writer.setSelection( paragraph, offset );
  63. *
  64. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  65. * that element and ends after the last child of that element.
  66. *
  67. * writer.setSelection( paragraph, 'in' );
  68. *
  69. * Creates a range on the {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
  70. *
  71. * writer.setSelection( paragraph, 'on' );
  72. *
  73. * // Removes all ranges.
  74. * writer.setSelection( null );
  75. *
  76. * `Writer#setSelection()` allow passing additional options (`backward`, `fake` and `label`) as the last argument.
  77. *
  78. * // Sets selection as backward.
  79. * writer.setSelection( range, { backward: true } );
  80. *
  81. * // Sets selection as fake.
  82. * // Fake selection does not render as browser native selection over selected elements and is hidden to the user.
  83. * // This way, no native selection UI artifacts are displayed to the user and selection over elements can be
  84. * // represented in other way, for example by applying proper CSS class.
  85. * writer.setSelection( range, { fake: true } );
  86. *
  87. * // Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
  88. * // (and be properly handled by screen readers).
  89. * writer.setSelection( range, { fake: true, label: 'foo' } );
  90. *
  91. * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
  92. * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
  93. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  94. * @param {Object} [options]
  95. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  96. * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
  97. * @param {String} [options.label] Label for the fake selection.
  98. */
  99. setSelection( selectable, placeOrOffset, options ) {
  100. this.document.selection._setTo( selectable, placeOrOffset, options );
  101. }
  102. /**
  103. * Moves {@link module:engine/view/selection~Selection#focus selection's focus} to the specified location.
  104. *
  105. * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
  106. *
  107. * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
  108. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  109. * first parameter is a {@link module:engine/view/item~Item view item}.
  110. */
  111. setSelectionFocus( itemOrPosition, offset ) {
  112. this.document.selection._setFocus( itemOrPosition, offset );
  113. }
  114. /**
  115. * Creates a new {@link module:engine/view/text~Text text node}.
  116. *
  117. * writer.createText( 'foo' );
  118. *
  119. * @param {String} data Text data.
  120. * @returns {module:engine/view/text~Text} Created text node.
  121. */
  122. createText( data ) {
  123. return new Text( data );
  124. }
  125. /**
  126. * Creates new {@link module:engine/view/attributeelement~AttributeElement}.
  127. *
  128. * writer.createAttributeElement( 'strong' );
  129. * writer.createAttributeElement( 'strong', { 'alignment': 'center' } );
  130. *
  131. * @param {String} name Name of the element.
  132. * @param {Object} [attributes] Elements attributes.
  133. * @returns {module:engine/view/attributeelement~AttributeElement} Created element.
  134. */
  135. createAttributeElement( name, attributes, priority ) {
  136. const attributeElement = new AttributeElement( name, attributes );
  137. if ( priority ) {
  138. attributeElement._priority = priority;
  139. }
  140. return attributeElement;
  141. }
  142. /**
  143. * Creates new {@link module:engine/view/containerelement~ContainerElement}.
  144. *
  145. * writer.createContainerElement( 'paragraph' );
  146. * writer.createContainerElement( 'paragraph', { 'alignment': 'center' } );
  147. *
  148. * @param {String} name Name of the element.
  149. * @param {Object} [attributes] Elements attributes.
  150. * @returns {module:engine/view/containerelement~ContainerElement} Created element.
  151. */
  152. createContainerElement( name, attributes ) {
  153. return new ContainerElement( name, attributes );
  154. }
  155. /**
  156. * Creates new {@link module:engine/view/editableelement~EditableElement}.
  157. *
  158. * writer.createEditableElement( document, 'div' );
  159. * writer.createEditableElement( document, 'div', { 'alignment': 'center' } );
  160. *
  161. * @param {module:engine/view/document~Document} document View document.
  162. * @param {String} name Name of the element.
  163. * @param {Object} [attributes] Elements attributes.
  164. * @returns {module:engine/view/editableelement~EditableElement} Created element.
  165. */
  166. createEditableElement( name, attributes ) {
  167. const editableElement = new EditableElement( name, attributes );
  168. editableElement._document = this.document;
  169. return editableElement;
  170. }
  171. /**
  172. * Creates new {@link module:engine/view/emptyelement~EmptyElement}.
  173. *
  174. * writer.createEmptyElement( 'img' );
  175. * writer.createEmptyElement( 'img', { 'alignment': 'center' } );
  176. *
  177. * @param {String} name Name of the element.
  178. * @param {Object} [attributes] Elements attributes.
  179. * @returns {module:engine/view/emptyelement~EmptyElement} Created element.
  180. */
  181. createEmptyElement( name, attributes ) {
  182. return new EmptyElement( name, attributes );
  183. }
  184. /**
  185. * Creates new {@link module:engine/view/uielement~UIElement}.
  186. *
  187. * writer.createUIElement( 'span' );
  188. * writer.createUIElement( 'span', { 'alignment': 'center' } );
  189. *
  190. * Custom render function can be provided as third parameter:
  191. *
  192. * writer.createUIElement( 'span', null, function( domDocument ) {
  193. * const domElement = this.toDomElement( domDocument );
  194. * domElement.innerHTML = '<b>this is ui element</b>';
  195. *
  196. * return domElement;
  197. * } );
  198. *
  199. * @param {String} name Name of the element.
  200. * @param {Object} [attributes] Elements attributes.
  201. * @param {Function} [renderFunction] Custom render function.
  202. * @returns {module:engine/view/uielement~UIElement} Created element.
  203. */
  204. createUIElement( name, attributes, renderFunction ) {
  205. const uiElement = new UIElement( name, attributes );
  206. if ( renderFunction ) {
  207. uiElement.render = renderFunction;
  208. }
  209. return uiElement;
  210. }
  211. /**
  212. * Sets the text content for the specified `textNode`.
  213. *
  214. * @param {String} value New value.
  215. * @param {module:engine/view/text~Text} textNode Text node that will be updated.
  216. */
  217. setTextData( value, textNode ) {
  218. textNode._data = value;
  219. }
  220. /**
  221. * Adds or overwrite element's attribute with a specified key and value.
  222. *
  223. * writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
  224. *
  225. * @param {String} key Attribute key.
  226. * @param {String} value Attribute value.
  227. * @param {module:engine/view/element~Element} element
  228. */
  229. setAttribute( key, value, element ) {
  230. element._setAttribute( key, value );
  231. }
  232. /**
  233. * Removes attribute from the element.
  234. *
  235. * writer.removeAttribute( 'href', linkElement );
  236. *
  237. * @param {String} key Attribute key.
  238. * @param {module:engine/view/element~Element} element
  239. */
  240. removeAttribute( key, element ) {
  241. element._removeAttribute( key );
  242. }
  243. /**
  244. * Adds specified class to the element.
  245. *
  246. * writer.addClass( 'foo', linkElement );
  247. * writer.addClass( [ 'foo', 'bar' ], linkElement );
  248. *
  249. * @param {Array.<String>|String} className
  250. * @param {module:engine/view/element~Element} element
  251. */
  252. addClass( className, element ) {
  253. element._addClass( className );
  254. }
  255. /**
  256. * Removes specified class from the element.
  257. *
  258. * writer.removeClass( 'foo', linkElement );
  259. * writer.removeClass( [ 'foo', 'bar' ], linkElement );
  260. *
  261. * @param {Array.<String>|String} className
  262. * @param {module:engine/view/element~Element} element
  263. */
  264. removeClass( className, element ) {
  265. element._removeClass( className );
  266. }
  267. /**
  268. * Adds style to the element.
  269. *
  270. * writer.setStyle( 'color', 'red', element );
  271. * writer.setStyle( {
  272. * color: 'red',
  273. * position: 'fixed'
  274. * }, element );
  275. *
  276. * @param {String|Object} property Property name or object with key - value pairs.
  277. * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
  278. * @param {module:engine/view/element~Element} element Element to set styles on.
  279. */
  280. setStyle( property, value, element ) {
  281. if ( isPlainObject( property ) && element === undefined ) {
  282. element = value;
  283. }
  284. element._setStyle( property, value );
  285. }
  286. /**
  287. * Removes specified style from the element.
  288. *
  289. * writer.removeStyle( 'color', element ); // Removes 'color' style.
  290. * writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
  291. *
  292. * @param {Array.<String>|String} property
  293. * @param {module:engine/view/element~Element} element
  294. */
  295. removeStyle( property, element ) {
  296. element._removeStyle( property );
  297. }
  298. /**
  299. * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
  300. * so they can be used to add special data to elements.
  301. *
  302. * @param {String|Symbol} key
  303. * @param {*} value
  304. * @param {module:engine/view/element~Element} element
  305. */
  306. setCustomProperty( key, value, element ) {
  307. element._setCustomProperty( key, value );
  308. }
  309. /**
  310. * Removes a custom property stored under the given key.
  311. *
  312. * @param {String|Symbol} key
  313. * @param {module:engine/view/element~Element} element
  314. * @returns {Boolean} Returns true if property was removed.
  315. */
  316. removeCustomProperty( key, element ) {
  317. return element._removeCustomProperty( key );
  318. }
  319. /**
  320. * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
  321. * up to a container element.
  322. *
  323. * In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes:
  324. *
  325. * <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
  326. * <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
  327. * <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
  328. * <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>
  329. *
  330. * **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
  331. *
  332. * **Note:** Difference between {@link module:engine/view/writer~Writer#breakAttributes breakAttributes} and
  333. * {@link module:engine/view/writer~Writer#breakContainer breakContainer} is that `breakAttributes` breaks all
  334. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
  335. * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
  336. * `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
  337. *
  338. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container`
  339. * when {@link module:engine/view/range~Range#start start}
  340. * and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
  341. *
  342. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element`
  343. * when trying to break attributes
  344. * inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
  345. *
  346. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element`
  347. * when trying to break attributes
  348. * inside {@link module:engine/view/uielement~UIElement UIElement}.
  349. *
  350. * @see module:engine/view/attributeelement~AttributeElement
  351. * @see module:engine/view/containerelement~ContainerElement
  352. * @see module:engine/view/writer~Writer#breakContainer
  353. * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where
  354. * to break attribute elements.
  355. * @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute
  356. * elements.
  357. */
  358. breakAttributes( positionOrRange ) {
  359. if ( positionOrRange instanceof Position ) {
  360. return _breakAttributes( positionOrRange );
  361. } else {
  362. return _breakAttributesRange( positionOrRange );
  363. }
  364. }
  365. /**
  366. * Breaks {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position. Position
  367. * has to be directly inside container element and cannot be in root. Does not break if position is at the beginning
  368. * or at the end of it's parent element.
  369. *
  370. * <p>foo^bar</p> -> <p>foo</p><p>bar</p>
  371. * <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
  372. * <p>^foobar</p> -> ^<p>foobar</p>
  373. * <p>foobar^</p> -> <p>foobar</p>^
  374. *
  375. * **Note:** Difference between {@link module:engine/view/writer~Writer#breakAttributes breakAttributes} and
  376. * {@link module:engine/view/writer~Writer#breakContainer breakContainer} is that `breakAttributes` breaks all
  377. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
  378. * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
  379. * `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
  380. *
  381. * @see module:engine/view/attributeelement~AttributeElement
  382. * @see module:engine/view/containerelement~ContainerElement
  383. * @see module:engine/view/writer~Writer#breakAttributes
  384. * @param {module:engine/view/position~Position} position Position where to break element.
  385. * @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken,
  386. * the returned position is placed either before it or after it.
  387. */
  388. breakContainer( position ) {
  389. const element = position.parent;
  390. if ( !( element.is( 'containerElement' ) ) ) {
  391. /**
  392. * Trying to break an element which is not a container element.
  393. *
  394. * @error view-writer-break-non-container-element
  395. */
  396. throw new CKEditorError(
  397. 'view-writer-break-non-container-element: Trying to break an element which is not a container element.'
  398. );
  399. }
  400. if ( !element.parent ) {
  401. /**
  402. * Trying to break root element.
  403. *
  404. * @error view-writer-break-root
  405. */
  406. throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
  407. }
  408. if ( position.isAtStart ) {
  409. return Position.createBefore( element );
  410. } else if ( !position.isAtEnd ) {
  411. const newElement = element._clone( false );
  412. this.insert( Position.createAfter( element ), newElement );
  413. const sourceRange = new Range( position, Position.createAt( element, 'end' ) );
  414. const targetPosition = new Position( newElement, 0 );
  415. this.move( sourceRange, targetPosition );
  416. }
  417. return Position.createAfter( element );
  418. }
  419. /**
  420. * Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
  421. * Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
  422. *
  423. * In following examples `<p>` is a container and `<b>` is an attribute element:
  424. *
  425. * <p>foo[]bar</p> -> <p>foo{}bar</p>
  426. * <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
  427. * <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
  428. *
  429. * It will also take care about empty attributes when merging:
  430. *
  431. * <p><b>[]</b></p> -> <p>[]</p>
  432. * <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
  433. *
  434. * **Note:** Difference between {@link module:engine/view/writer~Writer#mergeAttributes mergeAttributes} and
  435. * {@link module:engine/view/writer~Writer#mergeContainers mergeContainers} is that `mergeAttributes` merges two
  436. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
  437. * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
  438. *
  439. * @see module:engine/view/attributeelement~AttributeElement
  440. * @see module:engine/view/containerelement~ContainerElement
  441. * @see module:engine/view/writer~Writer#mergeContainers
  442. * @param {module:engine/view/position~Position} position Merge position.
  443. * @returns {module:engine/view/position~Position} Position after merge.
  444. */
  445. mergeAttributes( position ) {
  446. const positionOffset = position.offset;
  447. const positionParent = position.parent;
  448. // When inside text node - nothing to merge.
  449. if ( positionParent.is( 'text' ) ) {
  450. return position;
  451. }
  452. // When inside empty attribute - remove it.
  453. if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
  454. const parent = positionParent.parent;
  455. const offset = positionParent.index;
  456. positionParent._remove();
  457. return this.mergeAttributes( new Position( parent, offset ) );
  458. }
  459. const nodeBefore = positionParent.getChild( positionOffset - 1 );
  460. const nodeAfter = positionParent.getChild( positionOffset );
  461. // Position should be placed between two nodes.
  462. if ( !nodeBefore || !nodeAfter ) {
  463. return position;
  464. }
  465. // When position is between two text nodes.
  466. if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
  467. return mergeTextNodes( nodeBefore, nodeAfter );
  468. }
  469. // When selection is between two same attribute elements.
  470. else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
  471. // Move all children nodes from node placed after selection and remove that node.
  472. const count = nodeBefore.childCount;
  473. nodeBefore._appendChildren( nodeAfter.getChildren() );
  474. nodeAfter._remove();
  475. // New position is located inside the first node, before new nodes.
  476. // Call this method recursively to merge again if needed.
  477. return this.mergeAttributes( new Position( nodeBefore, count ) );
  478. }
  479. return position;
  480. }
  481. /**
  482. * Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
  483. * Precisely, the element after the position is removed and it's contents are moved to element before the position.
  484. *
  485. * <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
  486. * <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
  487. *
  488. * **Note:** Difference between {@link module:engine/view/writer~Writer#mergeAttributes mergeAttributes} and
  489. * {@link module:engine/view/writer~Writer#mergeContainers mergeContainers} is that `mergeAttributes` merges two
  490. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
  491. * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
  492. *
  493. * @see module:engine/view/attributeelement~AttributeElement
  494. * @see module:engine/view/containerelement~ContainerElement
  495. * @see module:engine/view/writer~Writer#mergeAttributes
  496. * @param {module:engine/view/position~Position} position Merge position.
  497. * @returns {module:engine/view/position~Position} Position after merge.
  498. */
  499. mergeContainers( position ) {
  500. const prev = position.nodeBefore;
  501. const next = position.nodeAfter;
  502. if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
  503. /**
  504. * Element before and after given position cannot be merged.
  505. *
  506. * @error view-writer-merge-containers-invalid-position
  507. */
  508. throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
  509. 'Element before and after given position cannot be merged.' );
  510. }
  511. const lastChild = prev.getChild( prev.childCount - 1 );
  512. const newPosition = lastChild instanceof Text ? Position.createAt( lastChild, 'end' ) : Position.createAt( prev, 'end' );
  513. this.move( Range.createIn( next ), Position.createAt( prev, 'end' ) );
  514. this.remove( Range.createOn( next ) );
  515. return newPosition;
  516. }
  517. /**
  518. * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  519. * and merging them afterwards.
  520. *
  521. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
  522. * contains instances that are not {@link module:engine/view/text~Text Texts},
  523. * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
  524. * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
  525. * {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
  526. * {@link module:engine/view/uielement~UIElement UIElements}.
  527. *
  528. * @param {module:engine/view/position~Position} position Insertion position.
  529. * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
  530. * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
  531. * module:engine/view/uielement~UIElement|Iterable.<module:engine/view/text~Text|
  532. * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
  533. * module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
  534. * @returns {module:engine/view/range~Range} Range around inserted nodes.
  535. */
  536. insert( position, nodes ) {
  537. nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
  538. // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
  539. validateNodesToInsert( nodes );
  540. const container = getParentContainer( position );
  541. if ( !container ) {
  542. /**
  543. * Position's parent container cannot be found.
  544. *
  545. * @error view-writer-invalid-position-container
  546. */
  547. throw new CKEditorError( 'view-writer-invalid-position-container' );
  548. }
  549. const insertionPosition = _breakAttributes( position, true );
  550. const length = container._insertChildren( insertionPosition.offset, nodes );
  551. const endPosition = insertionPosition.getShiftedBy( length );
  552. const start = this.mergeAttributes( insertionPosition );
  553. // When no nodes were inserted - return collapsed range.
  554. if ( length === 0 ) {
  555. return new Range( start, start );
  556. } else {
  557. // If start position was merged - move end position.
  558. if ( !start.isEqual( insertionPosition ) ) {
  559. endPosition.offset--;
  560. }
  561. const end = this.mergeAttributes( endPosition );
  562. return new Range( start, end );
  563. }
  564. }
  565. /**
  566. * Removes provided range from the container.
  567. *
  568. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  569. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  570. * same parent container.
  571. *
  572. * @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
  573. * to a collapsed range showing the new position.
  574. * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
  575. */
  576. remove( range ) {
  577. validateRangeContainer( range );
  578. // If range is collapsed - nothing to remove.
  579. if ( range.isCollapsed ) {
  580. return new DocumentFragment();
  581. }
  582. // Break attributes at range start and end.
  583. const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
  584. const parentContainer = breakStart.parent;
  585. const count = breakEnd.offset - breakStart.offset;
  586. // Remove nodes in range.
  587. const removed = parentContainer._removeChildren( breakStart.offset, count );
  588. // Merge after removing.
  589. const mergePosition = this.mergeAttributes( breakStart );
  590. range.start = mergePosition;
  591. range.end = Position.createFromPosition( mergePosition );
  592. // Return removed nodes.
  593. return new DocumentFragment( removed );
  594. }
  595. /**
  596. * Removes matching elements from given range.
  597. *
  598. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  599. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  600. * same parent container.
  601. *
  602. * @param {module:engine/view/range~Range} range Range to clear.
  603. * @param {module:engine/view/element~Element} element Element to remove.
  604. */
  605. clear( range, element ) {
  606. validateRangeContainer( range );
  607. // Create walker on given range.
  608. // We walk backward because when we remove element during walk it modifies range end position.
  609. const walker = range.getWalker( {
  610. direction: 'backward',
  611. ignoreElementEnd: true
  612. } );
  613. // Let's walk.
  614. for ( const current of walker ) {
  615. const item = current.item;
  616. let rangeToRemove;
  617. // When current item matches to the given element.
  618. if ( item.is( 'element' ) && element.isSimilar( item ) ) {
  619. // Create range on this element.
  620. rangeToRemove = Range.createOn( item );
  621. // When range starts inside Text or TextProxy element.
  622. } else if ( !current.nextPosition.isAfter( range.start ) && item.is( 'textProxy' ) ) {
  623. // We need to check if parent of this text matches to given element.
  624. const parentElement = item.getAncestors().find( ancestor => {
  625. return ancestor.is( 'element' ) && element.isSimilar( ancestor );
  626. } );
  627. // If it is then create range inside this element.
  628. if ( parentElement ) {
  629. rangeToRemove = Range.createIn( parentElement );
  630. }
  631. }
  632. // If we have found element to remove.
  633. if ( rangeToRemove ) {
  634. // We need to check if element range stick out of the given range and truncate if it is.
  635. if ( rangeToRemove.end.isAfter( range.end ) ) {
  636. rangeToRemove.end = range.end;
  637. }
  638. if ( rangeToRemove.start.isBefore( range.start ) ) {
  639. rangeToRemove.start = range.start;
  640. }
  641. // At the end we remove range with found element.
  642. this.remove( rangeToRemove );
  643. }
  644. }
  645. }
  646. /**
  647. * Moves nodes from provided range to target position.
  648. *
  649. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  650. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  651. * same parent container.
  652. *
  653. * @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
  654. * @param {module:engine/view/position~Position} targetPosition Position to insert.
  655. * @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
  656. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
  657. */
  658. move( sourceRange, targetPosition ) {
  659. let nodes;
  660. if ( targetPosition.isAfter( sourceRange.end ) ) {
  661. targetPosition = _breakAttributes( targetPosition, true );
  662. const parent = targetPosition.parent;
  663. const countBefore = parent.childCount;
  664. sourceRange = _breakAttributesRange( sourceRange, true );
  665. nodes = this.remove( sourceRange );
  666. targetPosition.offset += ( parent.childCount - countBefore );
  667. } else {
  668. nodes = this.remove( sourceRange );
  669. }
  670. return this.insert( targetPosition, nodes );
  671. }
  672. /**
  673. * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
  674. * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
  675. *
  676. * If a collapsed range was passed and is same as selection, the selection
  677. * will be moved to the inside of the wrapped attribute element.
  678. *
  679. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
  680. * when {@link module:engine/view/range~Range#start}
  681. * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
  682. *
  683. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  684. * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
  685. *
  686. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
  687. * is collapsed and different than view selection.
  688. *
  689. * @param {module:engine/view/range~Range} range Range to wrap.
  690. * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
  691. * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
  692. */
  693. wrap( range, attribute ) {
  694. if ( !( attribute instanceof AttributeElement ) ) {
  695. throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
  696. }
  697. validateRangeContainer( range );
  698. if ( !range.isCollapsed ) {
  699. // Non-collapsed range. Wrap it with the attribute element.
  700. return this._wrapRange( range, attribute );
  701. } else {
  702. // Collapsed range. Wrap position.
  703. let position = range.start;
  704. if ( position.parent.is( 'element' ) && !_hasNonUiChildren( position.parent ) ) {
  705. position = position.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
  706. }
  707. position = this._wrapPosition( position, attribute );
  708. const viewSelection = this.document.selection;
  709. // If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
  710. if ( viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) {
  711. this.setSelection( position );
  712. }
  713. return new Range( position );
  714. }
  715. }
  716. /**
  717. * Unwraps nodes within provided range from attribute element.
  718. *
  719. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  720. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  721. * same parent container.
  722. *
  723. * @param {module:engine/view/range~Range} range
  724. * @param {module:engine/view/attributeelement~AttributeElement} attribute
  725. */
  726. unwrap( range, attribute ) {
  727. if ( !( attribute instanceof AttributeElement ) ) {
  728. /**
  729. * Attribute element need to be instance of attribute element.
  730. *
  731. * @error view-writer-unwrap-invalid-attribute
  732. */
  733. throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' );
  734. }
  735. validateRangeContainer( range );
  736. // If range is collapsed - nothing to unwrap.
  737. if ( range.isCollapsed ) {
  738. return range;
  739. }
  740. // Break attributes at range start and end.
  741. const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
  742. // Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
  743. // For example:
  744. // <b class="foo bar" title="baz"></b> unwrap with: <b class="foo"></p> result: <b class"bar" title="baz"></b>
  745. if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
  746. const node = breakStart.nodeAfter;
  747. // Unwrap single attribute element.
  748. if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && this._unwrapAttributeElement( attribute, node ) ) {
  749. const start = this.mergeAttributes( breakStart );
  750. if ( !start.isEqual( breakStart ) ) {
  751. breakEnd.offset--;
  752. }
  753. const end = this.mergeAttributes( breakEnd );
  754. return new Range( start, end );
  755. }
  756. }
  757. const parentContainer = breakStart.parent;
  758. // Unwrap children located between break points.
  759. const newRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
  760. // Merge attributes at the both ends and return a new range.
  761. const start = this.mergeAttributes( newRange.start );
  762. // If start position was merged - move end position back.
  763. if ( !start.isEqual( newRange.start ) ) {
  764. newRange.end.offset--;
  765. }
  766. const end = this.mergeAttributes( newRange.end );
  767. return new Range( start, end );
  768. }
  769. /**
  770. * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
  771. * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
  772. * has renamed element as {@link module:engine/view/position~Position#parent a parent}.
  773. *
  774. * New element has to be created because `Element#tagName` property in DOM is readonly.
  775. *
  776. * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
  777. *
  778. * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
  779. * @param {String} newName New name for element.
  780. */
  781. rename( viewElement, newName ) {
  782. const newElement = new ContainerElement( newName, viewElement.getAttributes() );
  783. this.insert( Position.createAfter( viewElement ), newElement );
  784. this.move( Range.createIn( viewElement ), Position.createAt( newElement ) );
  785. this.remove( Range.createOn( viewElement ) );
  786. return newElement;
  787. }
  788. /**
  789. * Wraps children with provided `attribute`. Only children contained in `parent` element between
  790. * `startOffset` and `endOffset` will be wrapped.
  791. *
  792. * @private
  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. */
  798. _wrapChildren( parent, startOffset, endOffset, attribute ) {
  799. let i = startOffset;
  800. const wrapPositions = [];
  801. while ( i < endOffset ) {
  802. const child = parent.getChild( i );
  803. const isText = child.is( 'text' );
  804. const isAttribute = child.is( 'attributeElement' );
  805. const isEmpty = child.is( 'emptyElement' );
  806. const isUI = child.is( 'uiElement' );
  807. // Wrap text, empty elements, ui elements or attributes with higher or equal priority.
  808. if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
  809. // Clone attribute.
  810. const newAttribute = attribute._clone();
  811. // Wrap current node with new attribute;
  812. child._remove();
  813. newAttribute._appendChildren( child );
  814. parent._insertChildren( i, newAttribute );
  815. wrapPositions.push( new Position( parent, i ) );
  816. }
  817. // If other nested attribute is found start wrapping there.
  818. else if ( isAttribute ) {
  819. this._wrapChildren( child, 0, child.childCount, attribute );
  820. }
  821. i++;
  822. }
  823. // Merge at each wrap.
  824. let offsetChange = 0;
  825. for ( const 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 = this.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. /**
  841. * Unwraps children from provided `attribute`. Only children contained in `parent` element between
  842. * `startOffset` and `endOffset` will be unwrapped.
  843. *
  844. * @private
  845. * @param {module:engine/view/element~Element} parent
  846. * @param {Number} startOffset
  847. * @param {Number} endOffset
  848. * @param {module:engine/view/element~Element} attribute
  849. */
  850. _unwrapChildren( parent, startOffset, endOffset, attribute ) {
  851. let i = startOffset;
  852. const unwrapPositions = [];
  853. // Iterate over each element between provided offsets inside parent.
  854. while ( i < endOffset ) {
  855. const child = parent.getChild( i );
  856. // If attributes are the similar, then unwrap.
  857. if ( child.isSimilar( attribute ) ) {
  858. const unwrapped = child.getChildren();
  859. const count = child.childCount;
  860. // Replace wrapper element with its children
  861. child._remove();
  862. parent._insertChildren( i, unwrapped );
  863. // Save start and end position of moved items.
  864. unwrapPositions.push(
  865. new Position( parent, i ),
  866. new Position( parent, i + count )
  867. );
  868. // Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
  869. // elements.
  870. i += count;
  871. endOffset += count - 1;
  872. } else {
  873. // If other nested attribute is found start unwrapping there.
  874. if ( child.is( 'attributeElement' ) ) {
  875. this._unwrapChildren( child, 0, child.childCount, attribute );
  876. }
  877. i++;
  878. }
  879. }
  880. // Merge at each unwrap.
  881. let offsetChange = 0;
  882. for ( const position of unwrapPositions ) {
  883. position.offset -= offsetChange;
  884. // Do not merge with elements outside selected children.
  885. if ( position.offset == startOffset || position.offset == endOffset ) {
  886. continue;
  887. }
  888. const newPosition = this.mergeAttributes( position );
  889. // If nodes were merged - other merge offsets will change.
  890. if ( !newPosition.isEqual( position ) ) {
  891. offsetChange++;
  892. endOffset--;
  893. }
  894. }
  895. return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  896. }
  897. /**
  898. * Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
  899. * This method will also merge newly added attribute element with its siblings whenever possible.
  900. *
  901. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  902. * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
  903. *
  904. * @private
  905. * @param {module:engine/view/range~Range} range
  906. * @param {module:engine/view/attributeelement~AttributeElement} attribute
  907. * @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
  908. */
  909. _wrapRange( range, attribute ) {
  910. // Range is inside single attribute and spans on all children.
  911. if ( rangeSpansOnAllChildren( range ) && this._wrapAttributeElement( attribute, range.start.parent ) ) {
  912. const parent = range.start.parent;
  913. const end = this.mergeAttributes( Position.createAfter( parent ) );
  914. const start = this.mergeAttributes( Position.createBefore( parent ) );
  915. return new Range( start, end );
  916. }
  917. // Break attributes at range start and end.
  918. const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
  919. // Range around one element.
  920. if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
  921. const node = breakStart.nodeAfter;
  922. if ( node instanceof AttributeElement && this._wrapAttributeElement( attribute, node ) ) {
  923. const start = this.mergeAttributes( breakStart );
  924. if ( !start.isEqual( breakStart ) ) {
  925. breakEnd.offset--;
  926. }
  927. const end = this.mergeAttributes( breakEnd );
  928. return new Range( start, end );
  929. }
  930. }
  931. const parentContainer = breakStart.parent;
  932. // Unwrap children located between break points.
  933. const unwrappedRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
  934. // Wrap all children with attribute.
  935. const newRange = this._wrapChildren( parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
  936. // Merge attributes at the both ends and return a new range.
  937. const start = this.mergeAttributes( newRange.start );
  938. // If start position was merged - move end position back.
  939. if ( !start.isEqual( newRange.start ) ) {
  940. newRange.end.offset--;
  941. }
  942. const end = this.mergeAttributes( newRange.end );
  943. return new Range( start, end );
  944. }
  945. /**
  946. * Helper function for {@link #wrap}. Wraps position with provided attribute element.
  947. * This method will also merge newly added attribute element with its siblings whenever possible.
  948. *
  949. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  950. * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
  951. *
  952. * @private
  953. * @param {module:engine/view/position~Position} position
  954. * @param {module:engine/view/attributeelement~AttributeElement} attribute
  955. * @returns {module:engine/view/position~Position} New position after wrapping.
  956. */
  957. _wrapPosition( position, attribute ) {
  958. // Return same position when trying to wrap with attribute similar to position parent.
  959. if ( attribute.isSimilar( position.parent ) ) {
  960. return movePositionToTextNode( Position.createFromPosition( position ) );
  961. }
  962. // When position is inside text node - break it and place new position between two text nodes.
  963. if ( position.parent.is( 'text' ) ) {
  964. position = breakTextNode( position );
  965. }
  966. // Create fake element that will represent position, and will not be merged with other attributes.
  967. const fakePosition = this.createAttributeElement();
  968. fakePosition._priority = Number.POSITIVE_INFINITY;
  969. fakePosition.isSimilar = () => false;
  970. // Insert fake element in position location.
  971. position.parent._insertChildren( position.offset, fakePosition );
  972. // Range around inserted fake attribute element.
  973. const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
  974. // Wrap fake element with attribute (it will also merge if possible).
  975. this.wrap( wrapRange, attribute );
  976. // Remove fake element and place new position there.
  977. const newPosition = new Position( fakePosition.parent, fakePosition.index );
  978. fakePosition._remove();
  979. // If position is placed between text nodes - merge them and return position inside.
  980. const nodeBefore = newPosition.nodeBefore;
  981. const nodeAfter = newPosition.nodeAfter;
  982. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  983. return mergeTextNodes( nodeBefore, nodeAfter );
  984. }
  985. // If position is next to text node - move position inside.
  986. return movePositionToTextNode( newPosition );
  987. }
  988. /**
  989. * Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by
  990. * merging them if possible. When merging is possible - all attributes, styles and classes are moved from wrapper
  991. * element to element being wrapped.
  992. *
  993. * @private
  994. * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
  995. * @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element.
  996. * @returns {Boolean} Returns `true` if elements are merged.
  997. */
  998. _wrapAttributeElement( wrapper, toWrap ) {
  999. // Can't merge if name or priority differs.
  1000. if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
  1001. return false;
  1002. }
  1003. // Check if attributes can be merged.
  1004. for ( const key of wrapper.getAttributeKeys() ) {
  1005. // Classes and styles should be checked separately.
  1006. if ( key === 'class' || key === 'style' ) {
  1007. continue;
  1008. }
  1009. // If some attributes are different we cannot wrap.
  1010. if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  1011. return false;
  1012. }
  1013. }
  1014. // Check if styles can be merged.
  1015. for ( const key of wrapper.getStyleNames() ) {
  1016. if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  1017. return false;
  1018. }
  1019. }
  1020. // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
  1021. for ( const key of wrapper.getAttributeKeys() ) {
  1022. // Classes and styles should be checked separately.
  1023. if ( key === 'class' || key === 'style' ) {
  1024. continue;
  1025. }
  1026. // Move only these attributes that are not present - other are similar.
  1027. if ( !toWrap.hasAttribute( key ) ) {
  1028. this.setAttribute( key, wrapper.getAttribute( key ), toWrap );
  1029. }
  1030. }
  1031. for ( const key of wrapper.getStyleNames() ) {
  1032. if ( !toWrap.hasStyle( key ) ) {
  1033. this.setStyle( key, wrapper.getStyle( key ), toWrap );
  1034. }
  1035. }
  1036. for ( const key of wrapper.getClassNames() ) {
  1037. if ( !toWrap.hasClass( key ) ) {
  1038. this.addClass( key, toWrap );
  1039. }
  1040. }
  1041. return true;
  1042. }
  1043. /**
  1044. * Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing
  1045. * corresponding attributes, classes and styles. All attributes, classes and styles from wrapper should be present
  1046. * inside element being unwrapped.
  1047. *
  1048. * @private
  1049. * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
  1050. * @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
  1051. * @returns {Boolean} Returns `true` if elements are unwrapped.
  1052. **/
  1053. _unwrapAttributeElement( wrapper, toUnwrap ) {
  1054. // Can't unwrap if name or priority differs.
  1055. if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
  1056. return false;
  1057. }
  1058. // Check if AttributeElement has all wrapper attributes.
  1059. for ( const key of wrapper.getAttributeKeys() ) {
  1060. // Classes and styles should be checked separately.
  1061. if ( key === 'class' || key === 'style' ) {
  1062. continue;
  1063. }
  1064. // If some attributes are missing or different we cannot unwrap.
  1065. if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  1066. return false;
  1067. }
  1068. }
  1069. // Check if AttributeElement has all wrapper classes.
  1070. if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) {
  1071. return false;
  1072. }
  1073. // Check if AttributeElement has all wrapper styles.
  1074. for ( const key of wrapper.getStyleNames() ) {
  1075. // If some styles are missing or different we cannot unwrap.
  1076. if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  1077. return false;
  1078. }
  1079. }
  1080. // Remove all wrapper's attributes from unwrapped element.
  1081. for ( const key of wrapper.getAttributeKeys() ) {
  1082. // Classes and styles should be checked separately.
  1083. if ( key === 'class' || key === 'style' ) {
  1084. continue;
  1085. }
  1086. this.removeAttribute( key, toUnwrap );
  1087. }
  1088. // Remove all wrapper's classes from unwrapped element.
  1089. this.removeClass( Array.from( wrapper.getClassNames() ), toUnwrap );
  1090. // Remove all wrapper's styles from unwrapped element.
  1091. this.removeStyle( Array.from( wrapper.getStyleNames() ), toUnwrap );
  1092. return true;
  1093. }
  1094. }
  1095. // Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
  1096. function _hasNonUiChildren( parent ) {
  1097. return Array.from( parent.getChildren() ).some( child => !child.is( 'uiElement' ) );
  1098. }
  1099. /**
  1100. * Attribute element need to be instance of attribute element.
  1101. *
  1102. * @error view-writer-wrap-invalid-attribute
  1103. */
  1104. // Returns first parent container of specified {@link module:engine/view/position~Position Position}.
  1105. // Position's parent node is checked as first, then next parents are checked.
  1106. // Note that {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
  1107. //
  1108. // @param {module:engine/view/position~Position} position Position used as a start point to locate parent container.
  1109. // @returns {module:engine/view/containerelement~ContainerElement|module:engine/view/documentfragment~DocumentFragment|undefined}
  1110. // Parent container element or `undefined` if container is not found.
  1111. function getParentContainer( position ) {
  1112. let parent = position.parent;
  1113. while ( !isContainerOrFragment( parent ) ) {
  1114. if ( !parent ) {
  1115. return undefined;
  1116. }
  1117. parent = parent.parent;
  1118. }
  1119. return parent;
  1120. }
  1121. // Function used by both public breakAttributes (without splitting text nodes) and by other methods (with
  1122. // splitting text nodes).
  1123. //
  1124. // @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
  1125. // @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
  1126. // container element. This behavior will result in incorrect view state, but is needed by other view writing methods
  1127. // which then fixes view state. Defaults to `false`.
  1128. // @returns {module:engine/view/range~Range} New range with located at break positions.
  1129. function _breakAttributesRange( range, forceSplitText = false ) {
  1130. const rangeStart = range.start;
  1131. const rangeEnd = range.end;
  1132. validateRangeContainer( range );
  1133. // Break at the collapsed position. Return new collapsed range.
  1134. if ( range.isCollapsed ) {
  1135. const position = _breakAttributes( range.start, forceSplitText );
  1136. return new Range( position, position );
  1137. }
  1138. const breakEnd = _breakAttributes( rangeEnd, forceSplitText );
  1139. const count = breakEnd.parent.childCount;
  1140. const breakStart = _breakAttributes( rangeStart, forceSplitText );
  1141. // Calculate new break end offset.
  1142. breakEnd.offset += breakEnd.parent.childCount - count;
  1143. return new Range( breakStart, breakEnd );
  1144. }
  1145. // Function used by public breakAttributes (without splitting text nodes) and by other methods (with
  1146. // splitting text nodes).
  1147. //
  1148. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
  1149. // is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
  1150. //
  1151. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
  1152. // is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
  1153. //
  1154. // @param {module:engine/view/position~Position} position Position where to break attributes.
  1155. // @param {Boolean} [forceSplitText = false] If set to `true`, will break text nodes even if they are directly in
  1156. // container element. This behavior will result in incorrect view state, but is needed by other view writing methods
  1157. // which then fixes view state. Defaults to `false`.
  1158. // @returns {module:engine/view/position~Position} New position after breaking the attributes.
  1159. function _breakAttributes( position, forceSplitText = false ) {
  1160. const positionOffset = position.offset;
  1161. const positionParent = position.parent;
  1162. // If position is placed inside EmptyElement - throw an exception as we cannot break inside.
  1163. if ( position.parent.is( 'emptyElement' ) ) {
  1164. /**
  1165. * Cannot break inside EmptyElement instance.
  1166. *
  1167. * @error view-writer-cannot-break-empty-element
  1168. */
  1169. throw new CKEditorError( 'view-writer-cannot-break-empty-element' );
  1170. }
  1171. // If position is placed inside UIElement - throw an exception as we cannot break inside.
  1172. if ( position.parent.is( 'uiElement' ) ) {
  1173. /**
  1174. * Cannot break inside UIElement instance.
  1175. *
  1176. * @error view-writer-cannot-break-ui-element
  1177. */
  1178. throw new CKEditorError( 'view-writer-cannot-break-ui-element' );
  1179. }
  1180. // There are no attributes to break and text nodes breaking is not forced.
  1181. if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
  1182. return Position.createFromPosition( position );
  1183. }
  1184. // Position's parent is container, so no attributes to break.
  1185. if ( isContainerOrFragment( positionParent ) ) {
  1186. return Position.createFromPosition( position );
  1187. }
  1188. // Break text and start again in new position.
  1189. if ( positionParent.is( 'text' ) ) {
  1190. return _breakAttributes( breakTextNode( position ), forceSplitText );
  1191. }
  1192. const length = positionParent.childCount;
  1193. // <p>foo<b><u>bar{}</u></b></p>
  1194. // <p>foo<b><u>bar</u>[]</b></p>
  1195. // <p>foo<b><u>bar</u></b>[]</p>
  1196. if ( positionOffset == length ) {
  1197. const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
  1198. return _breakAttributes( newPosition, forceSplitText );
  1199. } else
  1200. // <p>foo<b><u>{}bar</u></b></p>
  1201. // <p>foo<b>[]<u>bar</u></b></p>
  1202. // <p>foo{}<b><u>bar</u></b></p>
  1203. if ( positionOffset === 0 ) {
  1204. const newPosition = new Position( positionParent.parent, positionParent.index );
  1205. return _breakAttributes( newPosition, forceSplitText );
  1206. }
  1207. // <p>foo<b><u>b{}ar</u></b></p>
  1208. // <p>foo<b><u>b[]ar</u></b></p>
  1209. // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
  1210. // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
  1211. else {
  1212. const offsetAfter = positionParent.index + 1;
  1213. // Break element.
  1214. const clonedNode = positionParent._clone();
  1215. // Insert cloned node to position's parent node.
  1216. positionParent.parent._insertChildren( offsetAfter, clonedNode );
  1217. // Get nodes to move.
  1218. const count = positionParent.childCount - positionOffset;
  1219. const nodesToMove = positionParent._removeChildren( positionOffset, count );
  1220. // Move nodes to cloned node.
  1221. clonedNode._appendChildren( nodesToMove );
  1222. // Create new position to work on.
  1223. const newPosition = new Position( positionParent.parent, offsetAfter );
  1224. return _breakAttributes( newPosition, forceSplitText );
  1225. }
  1226. }
  1227. // Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
  1228. // can be wrapped otuside second element. It is done by comparing elements'
  1229. // {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
  1230. // {@link module:engine/view/element~Element#getIdentity identities} are compared.
  1231. //
  1232. // @param {module:engine/view/attributeelement~AttributeElement} a
  1233. // @param {module:engine/view/attributeelement~AttributeElement} b
  1234. // @returns {Boolean}
  1235. function shouldABeOutsideB( a, b ) {
  1236. if ( a.priority < b.priority ) {
  1237. return true;
  1238. } else if ( a.priority > b.priority ) {
  1239. return false;
  1240. }
  1241. // When priorities are equal and names are different - use identities.
  1242. return a.getIdentity() < b.getIdentity();
  1243. }
  1244. // Returns new position that is moved to near text node. Returns same position if there is no text node before of after
  1245. // specified position.
  1246. //
  1247. // <p>foo[]</p> -> <p>foo{}</p>
  1248. // <p>[]foo</p> -> <p>{}foo</p>
  1249. //
  1250. // @param {module:engine/view/position~Position} position
  1251. // @returns {module:engine/view/position~Position} Position located inside text node or same position if there is no text nodes
  1252. // before or after position location.
  1253. function movePositionToTextNode( position ) {
  1254. const nodeBefore = position.nodeBefore;
  1255. if ( nodeBefore && nodeBefore.is( 'text' ) ) {
  1256. return new Position( nodeBefore, nodeBefore.data.length );
  1257. }
  1258. const nodeAfter = position.nodeAfter;
  1259. if ( nodeAfter && nodeAfter.is( 'text' ) ) {
  1260. return new Position( nodeAfter, 0 );
  1261. }
  1262. return position;
  1263. }
  1264. // Breaks text node into two text nodes when possible.
  1265. //
  1266. // <p>foo{}bar</p> -> <p>foo[]bar</p>
  1267. // <p>{}foobar</p> -> <p>[]foobar</p>
  1268. // <p>foobar{}</p> -> <p>foobar[]</p>
  1269. //
  1270. // @param {module:engine/view/position~Position} position Position that need to be placed inside text node.
  1271. // @returns {module:engine/view/position~Position} New position after breaking text node.
  1272. function breakTextNode( position ) {
  1273. if ( position.offset == position.parent.data.length ) {
  1274. return new Position( position.parent.parent, position.parent.index + 1 );
  1275. }
  1276. if ( position.offset === 0 ) {
  1277. return new Position( position.parent.parent, position.parent.index );
  1278. }
  1279. // Get part of the text that need to be moved.
  1280. const textToMove = position.parent.data.slice( position.offset );
  1281. // Leave rest of the text in position's parent.
  1282. position.parent._data = position.parent.data.slice( 0, position.offset );
  1283. // Insert new text node after position's parent text node.
  1284. position.parent.parent._insertChildren( position.parent.index + 1, new Text( textToMove ) );
  1285. // Return new position between two newly created text nodes.
  1286. return new Position( position.parent.parent, position.parent.index + 1 );
  1287. }
  1288. // Merges two text nodes into first node. Removes second node and returns merge position.
  1289. //
  1290. // @param {module:engine/view/text~Text} t1 First text node to merge. Data from second text node will be moved at the end of
  1291. // this text node.
  1292. // @param {module:engine/view/text~Text} t2 Second text node to merge. This node will be removed after merging.
  1293. // @returns {module:engine/view/position~Position} Position after merging text nodes.
  1294. function mergeTextNodes( t1, t2 ) {
  1295. // Merge text data into first text node and remove second one.
  1296. const nodeBeforeLength = t1.data.length;
  1297. t1._data += t2.data;
  1298. t2._remove();
  1299. return new Position( t1, nodeBeforeLength );
  1300. }
  1301. // Returns `true` if range is located in same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}
  1302. // (`start` and `end` positions are located inside same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}),
  1303. // starts on 0 offset and ends after last child node.
  1304. //
  1305. // @param {module:engine/view/range~Range} Range
  1306. // @returns {Boolean}
  1307. function rangeSpansOnAllChildren( range ) {
  1308. return range.start.parent == range.end.parent && range.start.parent.is( 'attributeElement' ) &&
  1309. range.start.offset === 0 && range.end.offset === range.start.parent.childCount;
  1310. }
  1311. // Checks if provided nodes are valid to insert. Checks if each node is an instance of
  1312. // {@link module:engine/view/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
  1313. // {@link module:engine/view/containerelement~ContainerElement ContainerElement},
  1314. // {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or
  1315. // {@link module:engine/view/uielement~UIElement UIElement}.
  1316. //
  1317. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
  1318. // contains instances that are not {@link module:engine/view/text~Text Texts},
  1319. // {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
  1320. // {@link module:engine/view/uielement~UIElement UIElements},
  1321. // {@link module:engine/view/attributeelement~AttributeElement AttributeElements} or
  1322. // {@link module:engine/view/containerelement~ContainerElement ContainerElements}.
  1323. //
  1324. // @param Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement
  1325. // |module:engine/view/containerelement~ContainerElement> nodes
  1326. function validateNodesToInsert( nodes ) {
  1327. for ( const node of nodes ) {
  1328. if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define
  1329. /**
  1330. * Inserted nodes should be valid to insert. of {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
  1331. * {@link module:engine/view/containerelement~ContainerElement ContainerElement},
  1332. * {@link module:engine/view/emptyelement~EmptyElement EmptyElement},
  1333. * {@link module:engine/view/uielement~UIElement UIElement}, {@link module:engine/view/text~Text Text}.
  1334. *
  1335. * @error view-writer-insert-invalid-node
  1336. */
  1337. throw new CKEditorError( 'view-writer-insert-invalid-node' );
  1338. }
  1339. if ( !node.is( 'text' ) ) {
  1340. validateNodesToInsert( node.getChildren() );
  1341. }
  1342. }
  1343. }
  1344. const validNodesToInsert = [ Text, AttributeElement, ContainerElement, EmptyElement, UIElement ];
  1345. // Checks if node is ContainerElement or DocumentFragment, because in most cases they should be treated the same way.
  1346. //
  1347. // @param {module:engine/view/node~Node} node
  1348. // @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment.
  1349. function isContainerOrFragment( node ) {
  1350. return node && ( node.is( 'containerElement' ) || node.is( 'documentFragment' ) );
  1351. }
  1352. // Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed
  1353. // inside same {@link module:engine/view/containerelement~ContainerElement container element}.
  1354. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
  1355. //
  1356. // @param {module:engine/view/range~Range} range
  1357. function validateRangeContainer( range ) {
  1358. const startContainer = getParentContainer( range.start );
  1359. const endContainer = getParentContainer( range.end );
  1360. if ( !startContainer || !endContainer || startContainer !== endContainer ) {
  1361. /**
  1362. * Range container is invalid. This can happen if {@link module:engine/view/range~Range#start range start} and
  1363. * {@link module:engine/view/range~Range#end range end} positions are not placed inside same container or
  1364. * parent container for these positions cannot be found.
  1365. *
  1366. * @error view-writer-invalid-range-container
  1367. */
  1368. throw new CKEditorError( 'view-writer-invalid-range-container' );
  1369. }
  1370. }