downcastwriter.js 74 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module module:engine/view/downcastwriter
  7. */
  8. import Position from './position';
  9. import Range from './range';
  10. import Selection from './selection';
  11. import ContainerElement from './containerelement';
  12. import AttributeElement from './attributeelement';
  13. import EmptyElement from './emptyelement';
  14. import UIElement from './uielement';
  15. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  16. import DocumentFragment from './documentfragment';
  17. import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
  18. import Text from './text';
  19. import EditableElement from './editableelement';
  20. import { isPlainObject } from 'lodash-es';
  21. /**
  22. * View downcast writer.
  23. *
  24. * It provides a set of methods used to manipulate view nodes.
  25. *
  26. * Do not create an instance of this writer manually. To modify a view structure, use
  27. * the {@link module:engine/view/view~View#change `View#change()`} block.
  28. *
  29. * The `DowncastWriter` is designed to work with semantic views which are the views that were/are being downcasted from the model.
  30. * To work with ordinary views (e.g. parsed from a pasted content) use the
  31. * {@link module:engine/view/upcastwriter~UpcastWriter upcast writer}.
  32. *
  33. * Read more about changing the view in the {@glink framework/guides/architecture/editing-engine#changing-the-view Changing the view}
  34. * section of the {@glink framework/guides/architecture/editing-engine Editing engine architecture} guide.
  35. */
  36. export default class DowncastWriter {
  37. /**
  38. * @param {module:engine/view/document~Document} document
  39. */
  40. constructor( document ) {
  41. /**
  42. * @readonly
  43. * @type {module:engine/view/document~Document}
  44. */
  45. this.document = document;
  46. /**
  47. * Holds references to the attribute groups that share the same {@link module:engine/view/attributeelement~AttributeElement#id id}.
  48. * The keys are `id`s, the values are `Set`s holding {@link module:engine/view/attributeelement~AttributeElement}s.
  49. *
  50. * @private
  51. * @type {Map.<String,Set>}
  52. */
  53. this._cloneGroups = new Map();
  54. }
  55. /**
  56. * Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the
  57. * specified location based on the given {@link module:engine/view/selection~Selectable selectable}.
  58. *
  59. * Usage:
  60. *
  61. * // Sets selection to the given range.
  62. * const range = writer.createRange( start, end );
  63. * writer.setSelection( range );
  64. *
  65. * // Sets backward selection to the given range.
  66. * const range = writer.createRange( start, end );
  67. * writer.setSelection( range );
  68. *
  69. * // Sets selection to given ranges.
  70. * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( start2, end2 ) ];
  71. * writer.setSelection( range );
  72. *
  73. * // Sets selection to the other selection.
  74. * const otherSelection = writer.createSelection();
  75. * writer.setSelection( otherSelection );
  76. *
  77. * // Sets collapsed selection at the given position.
  78. * const position = writer.createPositionFromPath( root, path );
  79. * writer.setSelection( position );
  80. *
  81. * // Sets collapsed selection at the position of given item and offset.
  82. * const paragraph = writer.createContainerElement( 'p' );
  83. * writer.setSelection( paragraph, offset );
  84. *
  85. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  86. * that element and ends after the last child of that element.
  87. *
  88. * writer.setSelection( paragraph, 'in' );
  89. *
  90. * Creates a range on the {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
  91. *
  92. * writer.setSelection( paragraph, 'on' );
  93. *
  94. * // Removes all ranges.
  95. * writer.setSelection( null );
  96. *
  97. * `DowncastWriter#setSelection()` allow passing additional options (`backward`, `fake` and `label`) as the last argument.
  98. *
  99. * // Sets selection as backward.
  100. * writer.setSelection( range, { backward: true } );
  101. *
  102. * // Sets selection as fake.
  103. * // Fake selection does not render as browser native selection over selected elements and is hidden to the user.
  104. * // This way, no native selection UI artifacts are displayed to the user and selection over elements can be
  105. * // represented in other way, for example by applying proper CSS class.
  106. * writer.setSelection( range, { fake: true } );
  107. *
  108. * // Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
  109. * // (and be properly handled by screen readers).
  110. * writer.setSelection( range, { fake: true, label: 'foo' } );
  111. *
  112. * @param {module:engine/view/selection~Selectable} selectable
  113. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  114. * @param {Object} [options]
  115. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  116. * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
  117. * @param {String} [options.label] Label for the fake selection.
  118. */
  119. setSelection( selectable, placeOrOffset, options ) {
  120. this.document.selection._setTo( selectable, placeOrOffset, options );
  121. }
  122. /**
  123. * Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location.
  124. *
  125. * The location can be specified in the same form as {@link module:engine/view/view~View#createPositionAt view.createPositionAt()}
  126. * parameters.
  127. *
  128. * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
  129. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  130. * first parameter is a {@link module:engine/view/item~Item view item}.
  131. */
  132. setSelectionFocus( itemOrPosition, offset ) {
  133. this.document.selection._setFocus( itemOrPosition, offset );
  134. }
  135. /**
  136. * Creates a new {@link module:engine/view/text~Text text node}.
  137. *
  138. * writer.createText( 'foo' );
  139. *
  140. * @param {String} data The text's data.
  141. * @returns {module:engine/view/text~Text} The created text node.
  142. */
  143. createText( data ) {
  144. return new Text( this.document, data );
  145. }
  146. /**
  147. * Creates new {@link module:engine/view/attributeelement~AttributeElement}.
  148. *
  149. * writer.createAttributeElement( 'strong' );
  150. * writer.createAttributeElement( 'a', { href: 'foo.bar' } );
  151. *
  152. * // Make `<a>` element contain other attributes element so the `<a>` element is not broken.
  153. * writer.createAttributeElement( 'a', { href: 'foo.bar' }, { priority: 5 } );
  154. *
  155. * // Set `id` of a marker element so it is not joined or merged with "normal" elements.
  156. * writer.createAttributeElement( 'span', { class: 'my-marker' }, { id: 'marker:my' } );
  157. *
  158. * @param {String} name Name of the element.
  159. * @param {Object} [attributes] Element's attributes.
  160. * @param {Object} [options] Element's options.
  161. * @param {Number} [options.priority] Element's {@link module:engine/view/attributeelement~AttributeElement#priority priority}.
  162. * @param {Number|String} [options.id] Element's {@link module:engine/view/attributeelement~AttributeElement#id id}.
  163. * @returns {module:engine/view/attributeelement~AttributeElement} Created element.
  164. */
  165. createAttributeElement( name, attributes, options = {} ) {
  166. const attributeElement = new AttributeElement( this.document, name, attributes );
  167. if ( options.priority ) {
  168. attributeElement._priority = options.priority;
  169. }
  170. if ( options.id ) {
  171. attributeElement._id = options.id;
  172. }
  173. return attributeElement;
  174. }
  175. /**
  176. * Creates new {@link module:engine/view/containerelement~ContainerElement}.
  177. *
  178. * writer.createContainerElement( 'p' );
  179. *
  180. * // Create element with custom attributes.
  181. * writer.createContainerElement( 'div', { id: 'foo-bar', 'data-baz': '123' } );
  182. *
  183. * // Create element with custom styles.
  184. * writer.createContainerElement( 'p', { style: 'font-weight: bold; padding-bottom: 10px' } );
  185. *
  186. * // Create element with custom classes.
  187. * writer.createContainerElement( 'p', { class: 'foo bar baz' } );
  188. *
  189. * @param {String} name Name of the element.
  190. * @param {Object} [attributes] Elements attributes.
  191. * @returns {module:engine/view/containerelement~ContainerElement} Created element.
  192. */
  193. createContainerElement( name, attributes ) {
  194. return new ContainerElement( this.document, name, attributes );
  195. }
  196. /**
  197. * Creates new {@link module:engine/view/editableelement~EditableElement}.
  198. *
  199. * writer.createEditableElement( 'div' );
  200. * writer.createEditableElement( 'div', { id: 'foo-1234' } );
  201. *
  202. * @param {String} name Name of the element.
  203. * @param {Object} [attributes] Elements attributes.
  204. * @returns {module:engine/view/editableelement~EditableElement} Created element.
  205. */
  206. createEditableElement( name, attributes ) {
  207. const editableElement = new EditableElement( this.document, name, attributes );
  208. editableElement._document = this.document;
  209. return editableElement;
  210. }
  211. /**
  212. * Creates new {@link module:engine/view/emptyelement~EmptyElement}.
  213. *
  214. * writer.createEmptyElement( 'img' );
  215. * writer.createEmptyElement( 'img', { id: 'foo-1234' } );
  216. *
  217. * @param {String} name Name of the element.
  218. * @param {Object} [attributes] Elements attributes.
  219. * @returns {module:engine/view/emptyelement~EmptyElement} Created element.
  220. */
  221. createEmptyElement( name, attributes ) {
  222. return new EmptyElement( this.document, name, attributes );
  223. }
  224. /**
  225. * Creates new {@link module:engine/view/uielement~UIElement}.
  226. *
  227. * writer.createUIElement( 'span' );
  228. * writer.createUIElement( 'span', { id: 'foo-1234' } );
  229. *
  230. * Custom render function can be provided as third parameter:
  231. *
  232. * writer.createUIElement( 'span', null, function( domDocument ) {
  233. * const domElement = this.toDomElement( domDocument );
  234. * domElement.innerHTML = '<b>this is ui element</b>';
  235. *
  236. * return domElement;
  237. * } );
  238. *
  239. * @param {String} name Name of the element.
  240. * @param {Object} [attributes] Elements attributes.
  241. * @param {Function} [renderFunction] Custom render function.
  242. * @returns {module:engine/view/uielement~UIElement} Created element.
  243. */
  244. createUIElement( name, attributes, renderFunction ) {
  245. const uiElement = new UIElement( this.document, name, attributes );
  246. if ( renderFunction ) {
  247. uiElement.render = renderFunction;
  248. }
  249. return uiElement;
  250. }
  251. /**
  252. * Adds or overwrite element's attribute with a specified key and value.
  253. *
  254. * writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
  255. *
  256. * @param {String} key Attribute key.
  257. * @param {String} value Attribute value.
  258. * @param {module:engine/view/element~Element} element
  259. */
  260. setAttribute( key, value, element ) {
  261. element._setAttribute( key, value );
  262. }
  263. /**
  264. * Removes attribute from the element.
  265. *
  266. * writer.removeAttribute( 'href', linkElement );
  267. *
  268. * @param {String} key Attribute key.
  269. * @param {module:engine/view/element~Element} element
  270. */
  271. removeAttribute( key, element ) {
  272. element._removeAttribute( key );
  273. }
  274. /**
  275. * Adds specified class to the element.
  276. *
  277. * writer.addClass( 'foo', linkElement );
  278. * writer.addClass( [ 'foo', 'bar' ], linkElement );
  279. *
  280. * @param {Array.<String>|String} className
  281. * @param {module:engine/view/element~Element} element
  282. */
  283. addClass( className, element ) {
  284. element._addClass( className );
  285. }
  286. /**
  287. * Removes specified class from the element.
  288. *
  289. * writer.removeClass( 'foo', linkElement );
  290. * writer.removeClass( [ 'foo', 'bar' ], linkElement );
  291. *
  292. * @param {Array.<String>|String} className
  293. * @param {module:engine/view/element~Element} element
  294. */
  295. removeClass( className, element ) {
  296. element._removeClass( className );
  297. }
  298. /**
  299. * Adds style to the element.
  300. *
  301. * writer.setStyle( 'color', 'red', element );
  302. * writer.setStyle( {
  303. * color: 'red',
  304. * position: 'fixed'
  305. * }, element );
  306. *
  307. * **Note**: The passed style can be normalized if
  308. * {@link module:engine/view/document~Document#addStyleProcessorRules a particular style processor rule is enabled}.
  309. * See {@link module:engine/view/stylesmap~StylesMap#set `StylesMap#set()`} for details.
  310. *
  311. * @param {String|Object} property Property name or object with key - value pairs.
  312. * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter.
  313. * @param {module:engine/view/element~Element} element Element to set styles on.
  314. */
  315. setStyle( property, value, element ) {
  316. if ( isPlainObject( property ) && element === undefined ) {
  317. element = value;
  318. }
  319. element._setStyle( property, value );
  320. }
  321. /**
  322. * Removes specified style from the element.
  323. *
  324. * writer.removeStyle( 'color', element ); // Removes 'color' style.
  325. * writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles.
  326. *
  327. * **Note**: This method can work with normalized style names if
  328. * {@link module:engine/view/document~Document#addStyleProcessorRules a particular style processor rule is enabled}.
  329. * See {@link module:engine/view/stylesmap~StylesMap#remove `StylesMap#remove()`} for details.
  330. *
  331. * @param {Array.<String>|String} property
  332. * @param {module:engine/view/element~Element} element
  333. */
  334. removeStyle( property, element ) {
  335. element._removeStyle( property );
  336. }
  337. /**
  338. * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM,
  339. * so they can be used to add special data to elements.
  340. *
  341. * @param {String|Symbol} key
  342. * @param {*} value
  343. * @param {module:engine/view/element~Element} element
  344. */
  345. setCustomProperty( key, value, element ) {
  346. element._setCustomProperty( key, value );
  347. }
  348. /**
  349. * Removes a custom property stored under the given key.
  350. *
  351. * @param {String|Symbol} key
  352. * @param {module:engine/view/element~Element} element
  353. * @returns {Boolean} Returns true if property was removed.
  354. */
  355. removeCustomProperty( key, element ) {
  356. return element._removeCustomProperty( key );
  357. }
  358. /**
  359. * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
  360. * up to a container element.
  361. *
  362. * In following examples `<p>` is a container, `<b>` and `<u>` are attribute nodes:
  363. *
  364. * <p>foo<b><u>bar{}</u></b></p> -> <p>foo<b><u>bar</u></b>[]</p>
  365. * <p>foo<b><u>{}bar</u></b></p> -> <p>foo{}<b><u>bar</u></b></p>
  366. * <p>foo<b><u>b{}ar</u></b></p> -> <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
  367. * <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>
  368. *
  369. * **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
  370. *
  371. * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes} and
  372. * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer} is that `breakAttributes` breaks all
  373. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
  374. * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
  375. * `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
  376. *
  377. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container`
  378. * when {@link module:engine/view/range~Range#start start}
  379. * and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
  380. *
  381. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element`
  382. * when trying to break attributes
  383. * inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
  384. *
  385. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element`
  386. * when trying to break attributes
  387. * inside {@link module:engine/view/uielement~UIElement UIElement}.
  388. *
  389. * @see module:engine/view/attributeelement~AttributeElement
  390. * @see module:engine/view/containerelement~ContainerElement
  391. * @see module:engine/view/downcastwriter~DowncastWriter#breakContainer
  392. * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where
  393. * to break attribute elements.
  394. * @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute
  395. * elements.
  396. */
  397. breakAttributes( positionOrRange ) {
  398. if ( positionOrRange instanceof Position ) {
  399. return this._breakAttributes( positionOrRange );
  400. } else {
  401. return this._breakAttributesRange( positionOrRange );
  402. }
  403. }
  404. /**
  405. * Breaks {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position. Position
  406. * has to be directly inside container element and cannot be in root. Does not break if position is at the beginning
  407. * or at the end of it's parent element.
  408. *
  409. * <p>foo^bar</p> -> <p>foo</p><p>bar</p>
  410. * <div><p>foo</p>^<p>bar</p></div> -> <div><p>foo</p></div><div><p>bar</p></div>
  411. * <p>^foobar</p> -> ^<p>foobar</p>
  412. * <p>foobar^</p> -> <p>foobar</p>^
  413. *
  414. * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes} and
  415. * {@link module:engine/view/downcastwriter~DowncastWriter#breakContainer breakContainer} is that `breakAttributes` breaks all
  416. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
  417. * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
  418. * `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
  419. *
  420. * @see module:engine/view/attributeelement~AttributeElement
  421. * @see module:engine/view/containerelement~ContainerElement
  422. * @see module:engine/view/downcastwriter~DowncastWriter#breakAttributes
  423. * @param {module:engine/view/position~Position} position Position where to break element.
  424. * @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken,
  425. * the returned position is placed either before it or after it.
  426. */
  427. breakContainer( position ) {
  428. const element = position.parent;
  429. if ( !( element.is( 'containerElement' ) ) ) {
  430. /**
  431. * Trying to break an element which is not a container element.
  432. *
  433. * @error view-writer-break-non-container-element
  434. */
  435. throw new CKEditorError(
  436. 'view-writer-break-non-container-element: Trying to break an element which is not a container element.',
  437. this.document
  438. );
  439. }
  440. if ( !element.parent ) {
  441. /**
  442. * Trying to break root element.
  443. *
  444. * @error view-writer-break-root
  445. */
  446. throw new CKEditorError( 'view-writer-break-root: Trying to break root element.', this.document );
  447. }
  448. if ( position.isAtStart ) {
  449. return Position._createBefore( element );
  450. } else if ( !position.isAtEnd ) {
  451. const newElement = element._clone( false );
  452. this.insert( Position._createAfter( element ), newElement );
  453. const sourceRange = new Range( position, Position._createAt( element, 'end' ) );
  454. const targetPosition = new Position( newElement, 0 );
  455. this.move( sourceRange, targetPosition );
  456. }
  457. return Position._createAfter( element );
  458. }
  459. /**
  460. * Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
  461. * Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
  462. *
  463. * In following examples `<p>` is a container and `<b>` is an attribute element:
  464. *
  465. * <p>foo[]bar</p> -> <p>foo{}bar</p>
  466. * <p><b>foo</b>[]<b>bar</b></p> -> <p><b>foo{}bar</b></p>
  467. * <p><b foo="bar">a</b>[]<b foo="baz">b</b></p> -> <p><b foo="bar">a</b>[]<b foo="baz">b</b></p>
  468. *
  469. * It will also take care about empty attributes when merging:
  470. *
  471. * <p><b>[]</b></p> -> <p>[]</p>
  472. * <p><b>foo</b><i>[]</i><b>bar</b></p> -> <p><b>foo{}bar</b></p>
  473. *
  474. * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
  475. * {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
  476. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
  477. * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
  478. *
  479. * @see module:engine/view/attributeelement~AttributeElement
  480. * @see module:engine/view/containerelement~ContainerElement
  481. * @see module:engine/view/downcastwriter~DowncastWriter#mergeContainers
  482. * @param {module:engine/view/position~Position} position Merge position.
  483. * @returns {module:engine/view/position~Position} Position after merge.
  484. */
  485. mergeAttributes( position ) {
  486. const positionOffset = position.offset;
  487. const positionParent = position.parent;
  488. // When inside text node - nothing to merge.
  489. if ( positionParent.is( 'text' ) ) {
  490. return position;
  491. }
  492. // When inside empty attribute - remove it.
  493. if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
  494. const parent = positionParent.parent;
  495. const offset = positionParent.index;
  496. positionParent._remove();
  497. this._removeFromClonedElementsGroup( positionParent );
  498. return this.mergeAttributes( new Position( parent, offset ) );
  499. }
  500. const nodeBefore = positionParent.getChild( positionOffset - 1 );
  501. const nodeAfter = positionParent.getChild( positionOffset );
  502. // Position should be placed between two nodes.
  503. if ( !nodeBefore || !nodeAfter ) {
  504. return position;
  505. }
  506. // When position is between two text nodes.
  507. if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
  508. return mergeTextNodes( nodeBefore, nodeAfter );
  509. }
  510. // When position is between two same attribute elements.
  511. else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
  512. // Move all children nodes from node placed after selection and remove that node.
  513. const count = nodeBefore.childCount;
  514. nodeBefore._appendChild( nodeAfter.getChildren() );
  515. nodeAfter._remove();
  516. this._removeFromClonedElementsGroup( nodeAfter );
  517. // New position is located inside the first node, before new nodes.
  518. // Call this method recursively to merge again if needed.
  519. return this.mergeAttributes( new Position( nodeBefore, count ) );
  520. }
  521. return position;
  522. }
  523. /**
  524. * Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
  525. * Precisely, the element after the position is removed and it's contents are moved to element before the position.
  526. *
  527. * <p>foo</p>^<p>bar</p> -> <p>foo^bar</p>
  528. * <div>foo</div>^<p>bar</p> -> <div>foo^bar</div>
  529. *
  530. * **Note:** Difference between {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes mergeAttributes} and
  531. * {@link module:engine/view/downcastwriter~DowncastWriter#mergeContainers mergeContainers} is that `mergeAttributes` merges two
  532. * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
  533. * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
  534. *
  535. * @see module:engine/view/attributeelement~AttributeElement
  536. * @see module:engine/view/containerelement~ContainerElement
  537. * @see module:engine/view/downcastwriter~DowncastWriter#mergeAttributes
  538. * @param {module:engine/view/position~Position} position Merge position.
  539. * @returns {module:engine/view/position~Position} Position after merge.
  540. */
  541. mergeContainers( position ) {
  542. const prev = position.nodeBefore;
  543. const next = position.nodeAfter;
  544. if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
  545. /**
  546. * Element before and after given position cannot be merged.
  547. *
  548. * @error view-writer-merge-containers-invalid-position
  549. */
  550. throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
  551. 'Element before and after given position cannot be merged.', this.document );
  552. }
  553. const lastChild = prev.getChild( prev.childCount - 1 );
  554. const newPosition = lastChild instanceof Text ? Position._createAt( lastChild, 'end' ) : Position._createAt( prev, 'end' );
  555. this.move( Range._createIn( next ), Position._createAt( prev, 'end' ) );
  556. this.remove( Range._createOn( next ) );
  557. return newPosition;
  558. }
  559. /**
  560. * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
  561. * and merging them afterwards.
  562. *
  563. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
  564. * contains instances that are not {@link module:engine/view/text~Text Texts},
  565. * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
  566. * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
  567. * {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
  568. * {@link module:engine/view/uielement~UIElement UIElements}.
  569. *
  570. * @param {module:engine/view/position~Position} position Insertion position.
  571. * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
  572. * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
  573. * module:engine/view/uielement~UIElement|Iterable.<module:engine/view/text~Text|
  574. * module:engine/view/attributeelement~AttributeElement|module:engine/view/containerelement~ContainerElement|
  575. * module:engine/view/emptyelement~EmptyElement|module:engine/view/uielement~UIElement>} nodes Node or nodes to insert.
  576. * @returns {module:engine/view/range~Range} Range around inserted nodes.
  577. */
  578. insert( position, nodes ) {
  579. nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
  580. // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
  581. validateNodesToInsert( nodes, this.document );
  582. const container = getParentContainer( position );
  583. if ( !container ) {
  584. /**
  585. * Position's parent container cannot be found.
  586. *
  587. * @error view-writer-invalid-position-container
  588. */
  589. throw new CKEditorError( 'view-writer-invalid-position-container', this.document );
  590. }
  591. const insertionPosition = this._breakAttributes( position, true );
  592. const length = container._insertChild( insertionPosition.offset, nodes );
  593. for ( const node of nodes ) {
  594. this._addToClonedElementsGroup( node );
  595. }
  596. const endPosition = insertionPosition.getShiftedBy( length );
  597. const start = this.mergeAttributes( insertionPosition );
  598. // When no nodes were inserted - return collapsed range.
  599. if ( length === 0 ) {
  600. return new Range( start, start );
  601. } else {
  602. // If start position was merged - move end position.
  603. if ( !start.isEqual( insertionPosition ) ) {
  604. endPosition.offset--;
  605. }
  606. const end = this.mergeAttributes( endPosition );
  607. return new Range( start, end );
  608. }
  609. }
  610. /**
  611. * Removes provided range from the container.
  612. *
  613. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  614. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  615. * same parent container.
  616. *
  617. * @param {module:engine/view/range~Range|module:engine/view/item~Item} rangeOrItem Range to remove from container
  618. * or an {@link module:engine/view/item~Item item} to remove. If range is provided, after removing, it will be updated
  619. * to a collapsed range showing the new position.
  620. * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
  621. */
  622. remove( rangeOrItem ) {
  623. const range = rangeOrItem instanceof Range ? rangeOrItem : Range._createOn( rangeOrItem );
  624. validateRangeContainer( range, this.document );
  625. // If range is collapsed - nothing to remove.
  626. if ( range.isCollapsed ) {
  627. return new DocumentFragment( this.document );
  628. }
  629. // Break attributes at range start and end.
  630. const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
  631. const parentContainer = breakStart.parent;
  632. const count = breakEnd.offset - breakStart.offset;
  633. // Remove nodes in range.
  634. const removed = parentContainer._removeChildren( breakStart.offset, count );
  635. for ( const node of removed ) {
  636. this._removeFromClonedElementsGroup( node );
  637. }
  638. // Merge after removing.
  639. const mergePosition = this.mergeAttributes( breakStart );
  640. range.start = mergePosition;
  641. range.end = mergePosition.clone();
  642. // Return removed nodes.
  643. return new DocumentFragment( this.document, removed );
  644. }
  645. /**
  646. * Removes matching elements from given range.
  647. *
  648. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  649. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  650. * same parent container.
  651. *
  652. * @param {module:engine/view/range~Range} range Range to clear.
  653. * @param {module:engine/view/element~Element} element Element to remove.
  654. */
  655. clear( range, element ) {
  656. validateRangeContainer( range, this.document );
  657. // Create walker on given range.
  658. // We walk backward because when we remove element during walk it modifies range end position.
  659. const walker = range.getWalker( {
  660. direction: 'backward',
  661. ignoreElementEnd: true
  662. } );
  663. // Let's walk.
  664. for ( const current of walker ) {
  665. const item = current.item;
  666. let rangeToRemove;
  667. // When current item matches to the given element.
  668. if ( item.is( 'element' ) && element.isSimilar( item ) ) {
  669. // Create range on this element.
  670. rangeToRemove = Range._createOn( item );
  671. // When range starts inside Text or TextProxy element.
  672. } else if ( !current.nextPosition.isAfter( range.start ) && item.is( 'textProxy' ) ) {
  673. // We need to check if parent of this text matches to given element.
  674. const parentElement = item.getAncestors().find( ancestor => {
  675. return ancestor.is( 'element' ) && element.isSimilar( ancestor );
  676. } );
  677. // If it is then create range inside this element.
  678. if ( parentElement ) {
  679. rangeToRemove = Range._createIn( parentElement );
  680. }
  681. }
  682. // If we have found element to remove.
  683. if ( rangeToRemove ) {
  684. // We need to check if element range stick out of the given range and truncate if it is.
  685. if ( rangeToRemove.end.isAfter( range.end ) ) {
  686. rangeToRemove.end = range.end;
  687. }
  688. if ( rangeToRemove.start.isBefore( range.start ) ) {
  689. rangeToRemove.start = range.start;
  690. }
  691. // At the end we remove range with found element.
  692. this.remove( rangeToRemove );
  693. }
  694. }
  695. }
  696. /**
  697. * Moves nodes from provided range to target position.
  698. *
  699. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  700. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  701. * same parent container.
  702. *
  703. * @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
  704. * @param {module:engine/view/position~Position} targetPosition Position to insert.
  705. * @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
  706. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
  707. */
  708. move( sourceRange, targetPosition ) {
  709. let nodes;
  710. if ( targetPosition.isAfter( sourceRange.end ) ) {
  711. targetPosition = this._breakAttributes( targetPosition, true );
  712. const parent = targetPosition.parent;
  713. const countBefore = parent.childCount;
  714. sourceRange = this._breakAttributesRange( sourceRange, true );
  715. nodes = this.remove( sourceRange );
  716. targetPosition.offset += ( parent.childCount - countBefore );
  717. } else {
  718. nodes = this.remove( sourceRange );
  719. }
  720. return this.insert( targetPosition, nodes );
  721. }
  722. /**
  723. * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
  724. * If a collapsed range is provided, it will be wrapped only if it is equal to view selection.
  725. *
  726. * If a collapsed range was passed and is same as selection, the selection
  727. * will be moved to the inside of the wrapped attribute element.
  728. *
  729. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
  730. * when {@link module:engine/view/range~Range#start}
  731. * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
  732. *
  733. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  734. * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
  735. *
  736. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-nonselection-collapsed-range` when passed range
  737. * is collapsed and different than view selection.
  738. *
  739. * @param {module:engine/view/range~Range} range Range to wrap.
  740. * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
  741. * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element.
  742. */
  743. wrap( range, attribute ) {
  744. if ( !( attribute instanceof AttributeElement ) ) {
  745. throw new CKEditorError( 'view-writer-wrap-invalid-attribute', this.document );
  746. }
  747. validateRangeContainer( range, this.document );
  748. if ( !range.isCollapsed ) {
  749. // Non-collapsed range. Wrap it with the attribute element.
  750. return this._wrapRange( range, attribute );
  751. } else {
  752. // Collapsed range. Wrap position.
  753. let position = range.start;
  754. if ( position.parent.is( 'element' ) && !_hasNonUiChildren( position.parent ) ) {
  755. position = position.getLastMatchingPosition( value => value.item.is( 'uiElement' ) );
  756. }
  757. position = this._wrapPosition( position, attribute );
  758. const viewSelection = this.document.selection;
  759. // If wrapping position is equal to view selection, move view selection inside wrapping attribute element.
  760. if ( viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) {
  761. this.setSelection( position );
  762. }
  763. return new Range( position );
  764. }
  765. }
  766. /**
  767. * Unwraps nodes within provided range from attribute element.
  768. *
  769. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
  770. * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
  771. * same parent container.
  772. *
  773. * @param {module:engine/view/range~Range} range
  774. * @param {module:engine/view/attributeelement~AttributeElement} attribute
  775. */
  776. unwrap( range, attribute ) {
  777. if ( !( attribute instanceof AttributeElement ) ) {
  778. /**
  779. * Attribute element need to be instance of attribute element.
  780. *
  781. * @error view-writer-unwrap-invalid-attribute
  782. */
  783. throw new CKEditorError( 'view-writer-unwrap-invalid-attribute', this.document );
  784. }
  785. validateRangeContainer( range, this.document );
  786. // If range is collapsed - nothing to unwrap.
  787. if ( range.isCollapsed ) {
  788. return range;
  789. }
  790. // Break attributes at range start and end.
  791. const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
  792. const parentContainer = breakStart.parent;
  793. // Unwrap children located between break points.
  794. const newRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
  795. // Merge attributes at the both ends and return a new range.
  796. const start = this.mergeAttributes( newRange.start );
  797. // If start position was merged - move end position back.
  798. if ( !start.isEqual( newRange.start ) ) {
  799. newRange.end.offset--;
  800. }
  801. const end = this.mergeAttributes( newRange.end );
  802. return new Range( start, end );
  803. }
  804. /**
  805. * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
  806. * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
  807. * has renamed element as {@link module:engine/view/position~Position#parent a parent}.
  808. *
  809. * New element has to be created because `Element#tagName` property in DOM is readonly.
  810. *
  811. * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
  812. *
  813. * @param {String} newName New name for element.
  814. * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
  815. */
  816. rename( newName, viewElement ) {
  817. const newElement = new ContainerElement( this.document, newName, viewElement.getAttributes() );
  818. this.insert( Position._createAfter( viewElement ), newElement );
  819. this.move( Range._createIn( viewElement ), Position._createAt( newElement, 0 ) );
  820. this.remove( Range._createOn( viewElement ) );
  821. return newElement;
  822. }
  823. /**
  824. * Cleans up memory by removing obsolete cloned elements group from the writer.
  825. *
  826. * Should be used whenever all {@link module:engine/view/attributeelement~AttributeElement attribute elements}
  827. * with the same {@link module:engine/view/attributeelement~AttributeElement#id id} are going to be removed from the view and
  828. * the group will no longer be needed.
  829. *
  830. * Cloned elements group are not removed automatically in case if the group is still needed after all its elements
  831. * were removed from the view.
  832. *
  833. * Keep in mind that group names are equal to the `id` property of the attribute element.
  834. *
  835. * @param {String} groupName Name of the group to clear.
  836. */
  837. clearClonedElementsGroup( groupName ) {
  838. this._cloneGroups.delete( groupName );
  839. }
  840. /**
  841. * Creates position at the given location. The location can be specified as:
  842. *
  843. * * a {@link module:engine/view/position~Position position},
  844. * * parent element and offset (offset defaults to `0`),
  845. * * parent element and `'end'` (sets position at the end of that element),
  846. * * {@link module:engine/view/item~Item view item} and `'before'` or `'after'` (sets position before or after given view item).
  847. *
  848. * This method is a shortcut to other constructors such as:
  849. *
  850. * * {@link #createPositionBefore},
  851. * * {@link #createPositionAfter},
  852. *
  853. * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
  854. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  855. * first parameter is a {@link module:engine/view/item~Item view item}.
  856. */
  857. createPositionAt( itemOrPosition, offset ) {
  858. return Position._createAt( itemOrPosition, offset );
  859. }
  860. /**
  861. * Creates a new position after given view item.
  862. *
  863. * @param {module:engine/view/item~Item} item View item after which the position should be located.
  864. * @returns {module:engine/view/position~Position}
  865. */
  866. createPositionAfter( item ) {
  867. return Position._createAfter( item );
  868. }
  869. /**
  870. * Creates a new position before given view item.
  871. *
  872. * @param {module:engine/view/item~Item} item View item before which the position should be located.
  873. * @returns {module:engine/view/position~Position}
  874. */
  875. createPositionBefore( item ) {
  876. return Position._createBefore( item );
  877. }
  878. /**
  879. * Creates a range spanning from `start` position to `end` position.
  880. *
  881. * **Note:** This factory method creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
  882. *
  883. * @param {module:engine/view/position~Position} start Start position.
  884. * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
  885. * @returns {module:engine/view/range~Range}
  886. */
  887. createRange( start, end ) {
  888. return new Range( start, end );
  889. }
  890. /**
  891. * Creates a range that starts before given {@link module:engine/view/item~Item view item} and ends after it.
  892. *
  893. * @param {module:engine/view/item~Item} item
  894. * @returns {module:engine/view/range~Range}
  895. */
  896. createRangeOn( item ) {
  897. return Range._createOn( item );
  898. }
  899. /**
  900. * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
  901. * that element and ends after the last child of that element.
  902. *
  903. * @param {module:engine/view/element~Element} element Element which is a parent for the range.
  904. * @returns {module:engine/view/range~Range}
  905. */
  906. createRangeIn( element ) {
  907. return Range._createIn( element );
  908. }
  909. /**
  910. Creates new {@link module:engine/view/selection~Selection} instance.
  911. *
  912. * // Creates empty selection without ranges.
  913. * const selection = writer.createSelection();
  914. *
  915. * // Creates selection at the given range.
  916. * const range = writer.createRange( start, end );
  917. * const selection = writer.createSelection( range );
  918. *
  919. * // Creates selection at the given ranges
  920. * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
  921. * const selection = writer.createSelection( ranges );
  922. *
  923. * // Creates selection from the other selection.
  924. * const otherSelection = writer.createSelection();
  925. * const selection = writer.createSelection( otherSelection );
  926. *
  927. * // Creates selection from the document selection.
  928. * const selection = writer.createSelection( editor.editing.view.document.selection );
  929. *
  930. * // Creates selection at the given position.
  931. * const position = writer.createPositionFromPath( root, path );
  932. * const selection = writer.createSelection( position );
  933. *
  934. * // Creates collapsed selection at the position of given item and offset.
  935. * const paragraph = writer.createContainerElement( 'p' );
  936. * const selection = writer.createSelection( paragraph, offset );
  937. *
  938. * // Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
  939. * // first child of that element and ends after the last child of that element.
  940. * const selection = writer.createSelection( paragraph, 'in' );
  941. *
  942. * // Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
  943. * // just after the item.
  944. * const selection = writer.createSelection( paragraph, 'on' );
  945. *
  946. * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
  947. *
  948. * // Creates backward selection.
  949. * const selection = writer.createSelection( range, { backward: true } );
  950. *
  951. * Fake selection does not render as browser native selection over selected elements and is hidden to the user.
  952. * This way, no native selection UI artifacts are displayed to the user and selection over elements can be
  953. * represented in other way, for example by applying proper CSS class.
  954. *
  955. * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM
  956. * (and be properly handled by screen readers).
  957. *
  958. * // Creates fake selection with label.
  959. * const selection = writer.createSelection( range, { fake: true, label: 'foo' } );
  960. *
  961. * @param {module:engine/view/selection~Selectable} [selectable=null]
  962. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
  963. * @param {Object} [options]
  964. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  965. * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
  966. * @param {String} [options.label] Label for the fake selection.
  967. * @returns {module:engine/view/selection~Selection}
  968. */
  969. createSelection( selectable, placeOrOffset, options ) {
  970. return new Selection( selectable, placeOrOffset, options );
  971. }
  972. /**
  973. * Wraps children with provided `wrapElement`. Only children contained in `parent` element between
  974. * `startOffset` and `endOffset` will be wrapped.
  975. *
  976. * @private
  977. * @param {module:engine/view/element~Element} parent
  978. * @param {Number} startOffset
  979. * @param {Number} endOffset
  980. * @param {module:engine/view/element~Element} wrapElement
  981. */
  982. _wrapChildren( parent, startOffset, endOffset, wrapElement ) {
  983. let i = startOffset;
  984. const wrapPositions = [];
  985. while ( i < endOffset ) {
  986. const child = parent.getChild( i );
  987. const isText = child.is( 'text' );
  988. const isAttribute = child.is( 'attributeElement' );
  989. const isEmpty = child.is( 'emptyElement' );
  990. const isUI = child.is( 'uiElement' );
  991. //
  992. // (In all examples, assume that `wrapElement` is `<span class="foo">` element.)
  993. //
  994. // Check if `wrapElement` can be joined with the wrapped element. One of requirements is having same name.
  995. // If possible, join elements.
  996. //
  997. // <p><span class="bar">abc</span></p> --> <p><span class="foo bar">abc</span></p>
  998. //
  999. if ( isAttribute && this._wrapAttributeElement( wrapElement, child ) ) {
  1000. wrapPositions.push( new Position( parent, i ) );
  1001. }
  1002. //
  1003. // Wrap the child if it is not an attribute element or if it is an attribute element that should be inside
  1004. // `wrapElement` (due to priority).
  1005. //
  1006. // <p>abc</p> --> <p><span class="foo">abc</span></p>
  1007. // <p><strong>abc</strong></p> --> <p><span class="foo"><strong>abc</strong></span></p>
  1008. //
  1009. else if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( wrapElement, child ) ) ) {
  1010. // Clone attribute.
  1011. const newAttribute = wrapElement._clone();
  1012. // Wrap current node with new attribute.
  1013. child._remove();
  1014. newAttribute._appendChild( child );
  1015. parent._insertChild( i, newAttribute );
  1016. this._addToClonedElementsGroup( newAttribute );
  1017. wrapPositions.push( new Position( parent, i ) );
  1018. }
  1019. //
  1020. // If other nested attribute is found and it wasn't wrapped (see above), continue wrapping inside it.
  1021. //
  1022. // <p><a href="foo.html">abc</a></p> --> <p><a href="foo.html"><span class="foo">abc</span></a></p>
  1023. //
  1024. else if ( isAttribute ) {
  1025. this._wrapChildren( child, 0, child.childCount, wrapElement );
  1026. }
  1027. i++;
  1028. }
  1029. // Merge at each wrap.
  1030. let offsetChange = 0;
  1031. for ( const position of wrapPositions ) {
  1032. position.offset -= offsetChange;
  1033. // Do not merge with elements outside selected children.
  1034. if ( position.offset == startOffset ) {
  1035. continue;
  1036. }
  1037. const newPosition = this.mergeAttributes( position );
  1038. // If nodes were merged - other merge offsets will change.
  1039. if ( !newPosition.isEqual( position ) ) {
  1040. offsetChange++;
  1041. endOffset--;
  1042. }
  1043. }
  1044. return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  1045. }
  1046. /**
  1047. * Unwraps children from provided `unwrapElement`. Only children contained in `parent` element between
  1048. * `startOffset` and `endOffset` will be unwrapped.
  1049. *
  1050. * @private
  1051. * @param {module:engine/view/element~Element} parent
  1052. * @param {Number} startOffset
  1053. * @param {Number} endOffset
  1054. * @param {module:engine/view/element~Element} unwrapElement
  1055. */
  1056. _unwrapChildren( parent, startOffset, endOffset, unwrapElement ) {
  1057. let i = startOffset;
  1058. const unwrapPositions = [];
  1059. // Iterate over each element between provided offsets inside parent.
  1060. // We don't use tree walker or range iterator because we will be removing and merging potentially multiple nodes,
  1061. // so it could get messy. It is safer to it manually in this case.
  1062. while ( i < endOffset ) {
  1063. const child = parent.getChild( i );
  1064. // Skip all text nodes. There should be no container element's here either.
  1065. if ( !child.is( 'attributeElement' ) ) {
  1066. i++;
  1067. continue;
  1068. }
  1069. //
  1070. // (In all examples, assume that `unwrapElement` is `<span class="foo">` element.)
  1071. //
  1072. // If the child is similar to the given attribute element, unwrap it - it will be completely removed.
  1073. //
  1074. // <p><span class="foo">abc</span>xyz</p> --> <p>abcxyz</p>
  1075. //
  1076. if ( child.isSimilar( unwrapElement ) ) {
  1077. const unwrapped = child.getChildren();
  1078. const count = child.childCount;
  1079. // Replace wrapper element with its children
  1080. child._remove();
  1081. parent._insertChild( i, unwrapped );
  1082. this._removeFromClonedElementsGroup( child );
  1083. // Save start and end position of moved items.
  1084. unwrapPositions.push(
  1085. new Position( parent, i ),
  1086. new Position( parent, i + count )
  1087. );
  1088. // Skip elements that were unwrapped. Assuming there won't be another element to unwrap in child elements.
  1089. i += count;
  1090. endOffset += count - 1;
  1091. continue;
  1092. }
  1093. //
  1094. // If the child is not similar but is an attribute element, try partial unwrapping - remove the same attributes/styles/classes.
  1095. // Partial unwrapping will happen only if the elements have the same name.
  1096. //
  1097. // <p><span class="foo bar">abc</span>xyz</p> --> <p><span class="bar">abc</span>xyz</p>
  1098. // <p><i class="foo">abc</i>xyz</p> --> <p><i class="foo">abc</i>xyz</p>
  1099. //
  1100. if ( this._unwrapAttributeElement( unwrapElement, child ) ) {
  1101. unwrapPositions.push(
  1102. new Position( parent, i ),
  1103. new Position( parent, i + 1 )
  1104. );
  1105. i++;
  1106. continue;
  1107. }
  1108. //
  1109. // If other nested attribute is found, look through it's children for elements to unwrap.
  1110. //
  1111. // <p><i><span class="foo">abc</span></i><p> --> <p><i>abc</i><p>
  1112. //
  1113. this._unwrapChildren( child, 0, child.childCount, unwrapElement );
  1114. i++;
  1115. }
  1116. // Merge at each unwrap.
  1117. let offsetChange = 0;
  1118. for ( const position of unwrapPositions ) {
  1119. position.offset -= offsetChange;
  1120. // Do not merge with elements outside selected children.
  1121. if ( position.offset == startOffset || position.offset == endOffset ) {
  1122. continue;
  1123. }
  1124. const newPosition = this.mergeAttributes( position );
  1125. // If nodes were merged - other merge offsets will change.
  1126. if ( !newPosition.isEqual( position ) ) {
  1127. offsetChange++;
  1128. endOffset--;
  1129. }
  1130. }
  1131. return Range._createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
  1132. }
  1133. /**
  1134. * Helper function for `view.writer.wrap`. Wraps range with provided attribute element.
  1135. * This method will also merge newly added attribute element with its siblings whenever possible.
  1136. *
  1137. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  1138. * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
  1139. *
  1140. * @private
  1141. * @param {module:engine/view/range~Range} range
  1142. * @param {module:engine/view/attributeelement~AttributeElement} attribute
  1143. * @returns {module:engine/view/range~Range} New range after wrapping, spanning over wrapping attribute element.
  1144. */
  1145. _wrapRange( range, attribute ) {
  1146. // Break attributes at range start and end.
  1147. const { start: breakStart, end: breakEnd } = this._breakAttributesRange( range, true );
  1148. const parentContainer = breakStart.parent;
  1149. // Wrap all children with attribute.
  1150. const newRange = this._wrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
  1151. // Merge attributes at the both ends and return a new range.
  1152. const start = this.mergeAttributes( newRange.start );
  1153. // If start position was merged - move end position back.
  1154. if ( !start.isEqual( newRange.start ) ) {
  1155. newRange.end.offset--;
  1156. }
  1157. const end = this.mergeAttributes( newRange.end );
  1158. return new Range( start, end );
  1159. }
  1160. /**
  1161. * Helper function for {@link #wrap}. Wraps position with provided attribute element.
  1162. * This method will also merge newly added attribute element with its siblings whenever possible.
  1163. *
  1164. * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
  1165. * an instance of {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
  1166. *
  1167. * @private
  1168. * @param {module:engine/view/position~Position} position
  1169. * @param {module:engine/view/attributeelement~AttributeElement} attribute
  1170. * @returns {module:engine/view/position~Position} New position after wrapping.
  1171. */
  1172. _wrapPosition( position, attribute ) {
  1173. // Return same position when trying to wrap with attribute similar to position parent.
  1174. if ( attribute.isSimilar( position.parent ) ) {
  1175. return movePositionToTextNode( position.clone() );
  1176. }
  1177. // When position is inside text node - break it and place new position between two text nodes.
  1178. if ( position.parent.is( 'text' ) ) {
  1179. position = breakTextNode( position );
  1180. }
  1181. // Create fake element that will represent position, and will not be merged with other attributes.
  1182. const fakePosition = this.createAttributeElement();
  1183. fakePosition._priority = Number.POSITIVE_INFINITY;
  1184. fakePosition.isSimilar = () => false;
  1185. // Insert fake element in position location.
  1186. position.parent._insertChild( position.offset, fakePosition );
  1187. // Range around inserted fake attribute element.
  1188. const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
  1189. // Wrap fake element with attribute (it will also merge if possible).
  1190. this.wrap( wrapRange, attribute );
  1191. // Remove fake element and place new position there.
  1192. const newPosition = new Position( fakePosition.parent, fakePosition.index );
  1193. fakePosition._remove();
  1194. // If position is placed between text nodes - merge them and return position inside.
  1195. const nodeBefore = newPosition.nodeBefore;
  1196. const nodeAfter = newPosition.nodeAfter;
  1197. if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
  1198. return mergeTextNodes( nodeBefore, nodeAfter );
  1199. }
  1200. // If position is next to text node - move position inside.
  1201. return movePositionToTextNode( newPosition );
  1202. }
  1203. /**
  1204. * Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by
  1205. * merging them if possible. When merging is possible - all attributes, styles and classes are moved from wrapper
  1206. * element to element being wrapped.
  1207. *
  1208. * @private
  1209. * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
  1210. * @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element.
  1211. * @returns {Boolean} Returns `true` if elements are merged.
  1212. */
  1213. _wrapAttributeElement( wrapper, toWrap ) {
  1214. if ( !canBeJoined( wrapper, toWrap ) ) {
  1215. return false;
  1216. }
  1217. // Can't merge if name or priority differs.
  1218. if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) {
  1219. return false;
  1220. }
  1221. // Check if attributes can be merged.
  1222. for ( const key of wrapper.getAttributeKeys() ) {
  1223. // Classes and styles should be checked separately.
  1224. if ( key === 'class' || key === 'style' ) {
  1225. continue;
  1226. }
  1227. // If some attributes are different we cannot wrap.
  1228. if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  1229. return false;
  1230. }
  1231. }
  1232. // Check if styles can be merged.
  1233. for ( const key of wrapper.getStyleNames() ) {
  1234. if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  1235. return false;
  1236. }
  1237. }
  1238. // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
  1239. for ( const key of wrapper.getAttributeKeys() ) {
  1240. // Classes and styles should be checked separately.
  1241. if ( key === 'class' || key === 'style' ) {
  1242. continue;
  1243. }
  1244. // Move only these attributes that are not present - other are similar.
  1245. if ( !toWrap.hasAttribute( key ) ) {
  1246. this.setAttribute( key, wrapper.getAttribute( key ), toWrap );
  1247. }
  1248. }
  1249. for ( const key of wrapper.getStyleNames() ) {
  1250. if ( !toWrap.hasStyle( key ) ) {
  1251. this.setStyle( key, wrapper.getStyle( key ), toWrap );
  1252. }
  1253. }
  1254. for ( const key of wrapper.getClassNames() ) {
  1255. if ( !toWrap.hasClass( key ) ) {
  1256. this.addClass( key, toWrap );
  1257. }
  1258. }
  1259. return true;
  1260. }
  1261. /**
  1262. * Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing
  1263. * corresponding attributes, classes and styles. All attributes, classes and styles from wrapper should be present
  1264. * inside element being unwrapped.
  1265. *
  1266. * @private
  1267. * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement.
  1268. * @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element.
  1269. * @returns {Boolean} Returns `true` if elements are unwrapped.
  1270. **/
  1271. _unwrapAttributeElement( wrapper, toUnwrap ) {
  1272. if ( !canBeJoined( wrapper, toUnwrap ) ) {
  1273. return false;
  1274. }
  1275. // Can't unwrap if name or priority differs.
  1276. if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) {
  1277. return false;
  1278. }
  1279. // Check if AttributeElement has all wrapper attributes.
  1280. for ( const key of wrapper.getAttributeKeys() ) {
  1281. // Classes and styles should be checked separately.
  1282. if ( key === 'class' || key === 'style' ) {
  1283. continue;
  1284. }
  1285. // If some attributes are missing or different we cannot unwrap.
  1286. if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) {
  1287. return false;
  1288. }
  1289. }
  1290. // Check if AttributeElement has all wrapper classes.
  1291. if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) {
  1292. return false;
  1293. }
  1294. // Check if AttributeElement has all wrapper styles.
  1295. for ( const key of wrapper.getStyleNames() ) {
  1296. // If some styles are missing or different we cannot unwrap.
  1297. if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) {
  1298. return false;
  1299. }
  1300. }
  1301. // Remove all wrapper's attributes from unwrapped element.
  1302. for ( const key of wrapper.getAttributeKeys() ) {
  1303. // Classes and styles should be checked separately.
  1304. if ( key === 'class' || key === 'style' ) {
  1305. continue;
  1306. }
  1307. this.removeAttribute( key, toUnwrap );
  1308. }
  1309. // Remove all wrapper's classes from unwrapped element.
  1310. this.removeClass( Array.from( wrapper.getClassNames() ), toUnwrap );
  1311. // Remove all wrapper's styles from unwrapped element.
  1312. this.removeStyle( Array.from( wrapper.getStyleNames() ), toUnwrap );
  1313. return true;
  1314. }
  1315. /**
  1316. * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
  1317. *
  1318. * @private
  1319. * @param {module:engine/view/range~Range} range Range which `start` and `end` positions will be used to break attributes.
  1320. * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
  1321. * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
  1322. * @returns {module:engine/view/range~Range} New range with located at break positions.
  1323. */
  1324. _breakAttributesRange( range, forceSplitText = false ) {
  1325. const rangeStart = range.start;
  1326. const rangeEnd = range.end;
  1327. validateRangeContainer( range, this.document );
  1328. // Break at the collapsed position. Return new collapsed range.
  1329. if ( range.isCollapsed ) {
  1330. const position = this._breakAttributes( range.start, forceSplitText );
  1331. return new Range( position, position );
  1332. }
  1333. const breakEnd = this._breakAttributes( rangeEnd, forceSplitText );
  1334. const count = breakEnd.parent.childCount;
  1335. const breakStart = this._breakAttributes( rangeStart, forceSplitText );
  1336. // Calculate new break end offset.
  1337. breakEnd.offset += breakEnd.parent.childCount - count;
  1338. return new Range( breakStart, breakEnd );
  1339. }
  1340. /**
  1341. * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at given position.
  1342. *
  1343. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element` when break position
  1344. * is placed inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
  1345. *
  1346. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element` when break position
  1347. * is placed inside {@link module:engine/view/uielement~UIElement UIElement}.
  1348. *
  1349. * @private
  1350. * @param {module:engine/view/position~Position} position Position where to break attributes.
  1351. * @param {Boolean} [forceSplitText=false] If set to `true`, will break text nodes even if they are directly in container element.
  1352. * This behavior will result in incorrect view state, but is needed by other view writing methods which then fixes view state.
  1353. * @returns {module:engine/view/position~Position} New position after breaking the attributes.
  1354. */
  1355. _breakAttributes( position, forceSplitText = false ) {
  1356. const positionOffset = position.offset;
  1357. const positionParent = position.parent;
  1358. // If position is placed inside EmptyElement - throw an exception as we cannot break inside.
  1359. if ( position.parent.is( 'emptyElement' ) ) {
  1360. /**
  1361. * Cannot break inside EmptyElement instance.
  1362. *
  1363. * @error view-writer-cannot-break-empty-element
  1364. */
  1365. throw new CKEditorError( 'view-writer-cannot-break-empty-element', this.document );
  1366. }
  1367. // If position is placed inside UIElement - throw an exception as we cannot break inside.
  1368. if ( position.parent.is( 'uiElement' ) ) {
  1369. /**
  1370. * Cannot break inside UIElement instance.
  1371. *
  1372. * @error view-writer-cannot-break-ui-element
  1373. */
  1374. throw new CKEditorError( 'view-writer-cannot-break-ui-element', this.document );
  1375. }
  1376. // There are no attributes to break and text nodes breaking is not forced.
  1377. if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
  1378. return position.clone();
  1379. }
  1380. // Position's parent is container, so no attributes to break.
  1381. if ( isContainerOrFragment( positionParent ) ) {
  1382. return position.clone();
  1383. }
  1384. // Break text and start again in new position.
  1385. if ( positionParent.is( 'text' ) ) {
  1386. return this._breakAttributes( breakTextNode( position ), forceSplitText );
  1387. }
  1388. const length = positionParent.childCount;
  1389. // <p>foo<b><u>bar{}</u></b></p>
  1390. // <p>foo<b><u>bar</u>[]</b></p>
  1391. // <p>foo<b><u>bar</u></b>[]</p>
  1392. if ( positionOffset == length ) {
  1393. const newPosition = new Position( positionParent.parent, positionParent.index + 1 );
  1394. return this._breakAttributes( newPosition, forceSplitText );
  1395. } else {
  1396. // <p>foo<b><u>{}bar</u></b></p>
  1397. // <p>foo<b>[]<u>bar</u></b></p>
  1398. // <p>foo{}<b><u>bar</u></b></p>
  1399. if ( positionOffset === 0 ) {
  1400. const newPosition = new Position( positionParent.parent, positionParent.index );
  1401. return this._breakAttributes( newPosition, forceSplitText );
  1402. }
  1403. // <p>foo<b><u>b{}ar</u></b></p>
  1404. // <p>foo<b><u>b[]ar</u></b></p>
  1405. // <p>foo<b><u>b</u>[]<u>ar</u></b></p>
  1406. // <p>foo<b><u>b</u></b>[]<b><u>ar</u></b></p>
  1407. else {
  1408. const offsetAfter = positionParent.index + 1;
  1409. // Break element.
  1410. const clonedNode = positionParent._clone();
  1411. // Insert cloned node to position's parent node.
  1412. positionParent.parent._insertChild( offsetAfter, clonedNode );
  1413. this._addToClonedElementsGroup( clonedNode );
  1414. // Get nodes to move.
  1415. const count = positionParent.childCount - positionOffset;
  1416. const nodesToMove = positionParent._removeChildren( positionOffset, count );
  1417. // Move nodes to cloned node.
  1418. clonedNode._appendChild( nodesToMove );
  1419. // Create new position to work on.
  1420. const newPosition = new Position( positionParent.parent, offsetAfter );
  1421. return this._breakAttributes( newPosition, forceSplitText );
  1422. }
  1423. }
  1424. }
  1425. /**
  1426. * Stores the information that an {@link module:engine/view/attributeelement~AttributeElement attribute element} was
  1427. * added to the tree. Saves the reference to the group in the given element and updates the group, so other elements
  1428. * from the group now keep a reference to the given attribute element.
  1429. *
  1430. * The clones group can be obtained using {@link module:engine/view/attributeelement~AttributeElement#getElementsWithSameId}.
  1431. *
  1432. * Does nothing if added element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
  1433. *
  1434. * @private
  1435. * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to save.
  1436. */
  1437. _addToClonedElementsGroup( element ) {
  1438. // Add only if the element is in document tree.
  1439. if ( !element.root.is( 'rootElement' ) ) {
  1440. return;
  1441. }
  1442. // Traverse the element's children recursively to find other attribute elements that also might got inserted.
  1443. // The loop is at the beginning so we can make fast returns later in the code.
  1444. if ( element.is( 'element' ) ) {
  1445. for ( const child of element.getChildren() ) {
  1446. this._addToClonedElementsGroup( child );
  1447. }
  1448. }
  1449. const id = element.id;
  1450. if ( !id ) {
  1451. return;
  1452. }
  1453. let group = this._cloneGroups.get( id );
  1454. if ( !group ) {
  1455. group = new Set();
  1456. this._cloneGroups.set( id, group );
  1457. }
  1458. group.add( element );
  1459. element._clonesGroup = group;
  1460. }
  1461. /**
  1462. * Removes all the information about the given {@link module:engine/view/attributeelement~AttributeElement attribute element}
  1463. * from its clones group.
  1464. *
  1465. * Keep in mind, that the element will still keep a reference to the group (but the group will not keep a reference to it).
  1466. * This allows to reference the whole group even if the element was already removed from the tree.
  1467. *
  1468. * Does nothing if the element has no {@link module:engine/view/attributeelement~AttributeElement#id id}.
  1469. *
  1470. * @private
  1471. * @param {module:engine/view/attributeelement~AttributeElement} element Attribute element to remove.
  1472. */
  1473. _removeFromClonedElementsGroup( element ) {
  1474. // Traverse the element's children recursively to find other attribute elements that also got removed.
  1475. // The loop is at the beginning so we can make fast returns later in the code.
  1476. if ( element.is( 'element' ) ) {
  1477. for ( const child of element.getChildren() ) {
  1478. this._removeFromClonedElementsGroup( child );
  1479. }
  1480. }
  1481. const id = element.id;
  1482. if ( !id ) {
  1483. return;
  1484. }
  1485. const group = this._cloneGroups.get( id );
  1486. if ( !group ) {
  1487. return;
  1488. }
  1489. group.delete( element );
  1490. // Not removing group from element on purpose!
  1491. // If other parts of code have reference to this element, they will be able to get references to other elements from the group.
  1492. }
  1493. }
  1494. // Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements.
  1495. function _hasNonUiChildren( parent ) {
  1496. return Array.from( parent.getChildren() ).some( child => !child.is( 'uiElement' ) );
  1497. }
  1498. /**
  1499. * Attribute element need to be instance of attribute element.
  1500. *
  1501. * @error view-writer-wrap-invalid-attribute
  1502. */
  1503. // Returns first parent container of specified {@link module:engine/view/position~Position Position}.
  1504. // Position's parent node is checked as first, then next parents are checked.
  1505. // Note that {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
  1506. //
  1507. // @param {module:engine/view/position~Position} position Position used as a start point to locate parent container.
  1508. // @returns {module:engine/view/containerelement~ContainerElement|module:engine/view/documentfragment~DocumentFragment|undefined}
  1509. // Parent container element or `undefined` if container is not found.
  1510. function getParentContainer( position ) {
  1511. let parent = position.parent;
  1512. while ( !isContainerOrFragment( parent ) ) {
  1513. if ( !parent ) {
  1514. return undefined;
  1515. }
  1516. parent = parent.parent;
  1517. }
  1518. return parent;
  1519. }
  1520. // Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
  1521. // can be wrapped otuside second element. It is done by comparing elements'
  1522. // {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
  1523. // {@link module:engine/view/element~Element#getIdentity identities} are compared.
  1524. //
  1525. // @param {module:engine/view/attributeelement~AttributeElement} a
  1526. // @param {module:engine/view/attributeelement~AttributeElement} b
  1527. // @returns {Boolean}
  1528. function shouldABeOutsideB( a, b ) {
  1529. if ( a.priority < b.priority ) {
  1530. return true;
  1531. } else if ( a.priority > b.priority ) {
  1532. return false;
  1533. }
  1534. // When priorities are equal and names are different - use identities.
  1535. return a.getIdentity() < b.getIdentity();
  1536. }
  1537. // Returns new position that is moved to near text node. Returns same position if there is no text node before of after
  1538. // specified position.
  1539. //
  1540. // <p>foo[]</p> -> <p>foo{}</p>
  1541. // <p>[]foo</p> -> <p>{}foo</p>
  1542. //
  1543. // @param {module:engine/view/position~Position} position
  1544. // @returns {module:engine/view/position~Position} Position located inside text node or same position if there is no text nodes
  1545. // before or after position location.
  1546. function movePositionToTextNode( position ) {
  1547. const nodeBefore = position.nodeBefore;
  1548. if ( nodeBefore && nodeBefore.is( 'text' ) ) {
  1549. return new Position( nodeBefore, nodeBefore.data.length );
  1550. }
  1551. const nodeAfter = position.nodeAfter;
  1552. if ( nodeAfter && nodeAfter.is( 'text' ) ) {
  1553. return new Position( nodeAfter, 0 );
  1554. }
  1555. return position;
  1556. }
  1557. // Breaks text node into two text nodes when possible.
  1558. //
  1559. // <p>foo{}bar</p> -> <p>foo[]bar</p>
  1560. // <p>{}foobar</p> -> <p>[]foobar</p>
  1561. // <p>foobar{}</p> -> <p>foobar[]</p>
  1562. //
  1563. // @param {module:engine/view/position~Position} position Position that need to be placed inside text node.
  1564. // @returns {module:engine/view/position~Position} New position after breaking text node.
  1565. function breakTextNode( position ) {
  1566. if ( position.offset == position.parent.data.length ) {
  1567. return new Position( position.parent.parent, position.parent.index + 1 );
  1568. }
  1569. if ( position.offset === 0 ) {
  1570. return new Position( position.parent.parent, position.parent.index );
  1571. }
  1572. // Get part of the text that need to be moved.
  1573. const textToMove = position.parent.data.slice( position.offset );
  1574. // Leave rest of the text in position's parent.
  1575. position.parent._data = position.parent.data.slice( 0, position.offset );
  1576. // Insert new text node after position's parent text node.
  1577. position.parent.parent._insertChild( position.parent.index + 1, new Text( position.parent.document, textToMove ) );
  1578. // Return new position between two newly created text nodes.
  1579. return new Position( position.parent.parent, position.parent.index + 1 );
  1580. }
  1581. // Merges two text nodes into first node. Removes second node and returns merge position.
  1582. //
  1583. // @param {module:engine/view/text~Text} t1 First text node to merge. Data from second text node will be moved at the end of
  1584. // this text node.
  1585. // @param {module:engine/view/text~Text} t2 Second text node to merge. This node will be removed after merging.
  1586. // @returns {module:engine/view/position~Position} Position after merging text nodes.
  1587. function mergeTextNodes( t1, t2 ) {
  1588. // Merge text data into first text node and remove second one.
  1589. const nodeBeforeLength = t1.data.length;
  1590. t1._data += t2.data;
  1591. t2._remove();
  1592. return new Position( t1, nodeBeforeLength );
  1593. }
  1594. // Checks if provided nodes are valid to insert. Checks if each node is an instance of
  1595. // {@link module:engine/view/text~Text Text} or {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
  1596. // {@link module:engine/view/containerelement~ContainerElement ContainerElement},
  1597. // {@link module:engine/view/emptyelement~EmptyElement EmptyElement} or
  1598. // {@link module:engine/view/uielement~UIElement UIElement}.
  1599. //
  1600. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
  1601. // contains instances that are not {@link module:engine/view/text~Text Texts},
  1602. // {@link module:engine/view/emptyelement~EmptyElement EmptyElements},
  1603. // {@link module:engine/view/uielement~UIElement UIElements},
  1604. // {@link module:engine/view/attributeelement~AttributeElement AttributeElements} or
  1605. // {@link module:engine/view/containerelement~ContainerElement ContainerElements}.
  1606. //
  1607. // @param Iterable.<module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement
  1608. // |module:engine/view/containerelement~ContainerElement> nodes
  1609. // @param {Object} errorContext
  1610. function validateNodesToInsert( nodes, errorContext ) {
  1611. for ( const node of nodes ) {
  1612. if ( !validNodesToInsert.some( ( validNode => node instanceof validNode ) ) ) { // eslint-disable-line no-use-before-define
  1613. /**
  1614. * Inserted nodes should be valid to insert. of {@link module:engine/view/attributeelement~AttributeElement AttributeElement},
  1615. * {@link module:engine/view/containerelement~ContainerElement ContainerElement},
  1616. * {@link module:engine/view/emptyelement~EmptyElement EmptyElement},
  1617. * {@link module:engine/view/uielement~UIElement UIElement}, {@link module:engine/view/text~Text Text}.
  1618. *
  1619. * @error view-writer-insert-invalid-node
  1620. */
  1621. throw new CKEditorError( 'view-writer-insert-invalid-node', errorContext );
  1622. }
  1623. if ( !node.is( 'text' ) ) {
  1624. validateNodesToInsert( node.getChildren(), errorContext );
  1625. }
  1626. }
  1627. }
  1628. const validNodesToInsert = [ Text, AttributeElement, ContainerElement, EmptyElement, UIElement ];
  1629. // Checks if node is ContainerElement or DocumentFragment, because in most cases they should be treated the same way.
  1630. //
  1631. // @param {module:engine/view/node~Node} node
  1632. // @returns {Boolean} Returns `true` if node is instance of ContainerElement or DocumentFragment.
  1633. function isContainerOrFragment( node ) {
  1634. return node && ( node.is( 'containerElement' ) || node.is( 'documentFragment' ) );
  1635. }
  1636. // Checks if {@link module:engine/view/range~Range#start range start} and {@link module:engine/view/range~Range#end range end} are placed
  1637. // inside same {@link module:engine/view/containerelement~ContainerElement container element}.
  1638. // Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when validation fails.
  1639. //
  1640. // @param {module:engine/view/range~Range} range
  1641. // @param {Object} errorContext
  1642. function validateRangeContainer( range, errorContext ) {
  1643. const startContainer = getParentContainer( range.start );
  1644. const endContainer = getParentContainer( range.end );
  1645. if ( !startContainer || !endContainer || startContainer !== endContainer ) {
  1646. /**
  1647. * Range container is invalid. This can happen if {@link module:engine/view/range~Range#start range start} and
  1648. * {@link module:engine/view/range~Range#end range end} positions are not placed inside same container or
  1649. * parent container for these positions cannot be found.
  1650. *
  1651. * @error view-writer-invalid-range-container
  1652. */
  1653. throw new CKEditorError( 'view-writer-invalid-range-container', errorContext );
  1654. }
  1655. }
  1656. // Checks if two attribute elements can be joined together. Elements can be joined together if, and only if
  1657. // they do not have ids specified.
  1658. //
  1659. // @private
  1660. // @param {module:engine/view/element~Element} a
  1661. // @param {module:engine/view/element~Element} b
  1662. // @returns {Boolean}
  1663. function canBeJoined( a, b ) {
  1664. return a.id === null && b.id === null;
  1665. }