8
0

writer.js 61 KB

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