writer.js 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252
  1. /**
  2. * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/writer
  7. */
  8. import AttributeDelta from './delta/attributedelta';
  9. import InsertDelta from './delta/insertdelta';
  10. import MarkerDelta from './delta/markerdelta';
  11. import MergeDelta from './delta/mergedelta';
  12. import MoveDelta from './delta/movedelta';
  13. import RemoveDelta from './delta/removedelta';
  14. import RenameDelta from './delta/renamedelta';
  15. import RootAttributeDelta from './delta/rootattributedelta';
  16. import SplitDelta from './delta/splitdelta';
  17. import UnwrapDelta from './delta/unwrapdelta';
  18. import WeakInsertDelta from './delta/weakinsertdelta';
  19. import WrapDelta from './delta/wrapdelta';
  20. import AttributeOperation from './operation/attributeoperation';
  21. import DetachOperation from './operation/detachoperation';
  22. import InsertOperation from './operation/insertoperation';
  23. import MarkerOperation from './operation/markeroperation';
  24. import MoveOperation from './operation/moveoperation';
  25. import RemoveOperation from './operation/removeoperation';
  26. import RenameOperation from './operation/renameoperation';
  27. import RootAttributeOperation from './operation/rootattributeoperation';
  28. import DocumentFragment from './documentfragment';
  29. import Text from './text';
  30. import Element from './element';
  31. import RootElement from './rootelement';
  32. import Position from './position';
  33. import Range from './range.js';
  34. import DocumentSelection from './documentselection';
  35. import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  36. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  37. import uid from '@ckeditor/ckeditor5-utils/src/uid';
  38. /**
  39. * Model writer it the proper way of modifying model. It should be used whenever you wants to create node, modify
  40. * child nodes, attributes or text. To get writer use {@link module:engine/model/model~Model#change} or
  41. * {@link @see module:engine/model/model~Model#enqueueChange}.
  42. *
  43. * model.change( writer => {
  44. * writer.insertText( 'foo', paragraph, 'end' );
  45. * } );
  46. *
  47. * Note that writer can be passed to a nested function but you should never store and use it outside the `change` or
  48. * `enqueueChange` block.
  49. *
  50. * @see module:engine/model/model~Model#change
  51. * @see module:engine/model/model~Model#enqueueChange
  52. */
  53. export default class Writer {
  54. /**
  55. * Writer class constructor.
  56. *
  57. * It is not recommended to use it directly, use {@link module:engine/model/model~Model#change} or
  58. * {@link module:engine/model/model~Model#enqueueChange} instead.
  59. *
  60. * @protected
  61. * @param {module:engine/model/model~Model} model
  62. * @param {module:engine/model/batch~Batch} batch
  63. */
  64. constructor( model, batch ) {
  65. /**
  66. * @readonly
  67. * @type {module:engine/model/model~Model}
  68. */
  69. this.model = model;
  70. /**
  71. * @readonly
  72. * @type {module:engine/model/batch~Batch}
  73. */
  74. this.batch = batch;
  75. }
  76. /**
  77. * Creates a new {@link module:engine/model/text~Text text node}.
  78. *
  79. * writer.createText( 'foo' );
  80. * writer.createText( 'foo', { 'bold': true } );
  81. *
  82. * @param {String} data Text data.
  83. * @param {Object} [attributes] Text attributes.
  84. * @returns {module:engine/model/text~Text} Created text node.
  85. */
  86. createText( data, attributes ) {
  87. return new Text( data, attributes );
  88. }
  89. /**
  90. * Creates a new {@link module:engine/model/element~Element element}.
  91. *
  92. * writer.createElement( 'paragraph' );
  93. * writer.createElement( 'paragraph', { 'alignment': 'center' } );
  94. *
  95. * @param {String} name Name of the element.
  96. * @param {Object} [attributes] Elements attributes.
  97. * @returns {module:engine/model/element~Element} Created element.
  98. */
  99. createElement( name, attributes ) {
  100. return new Element( name, attributes );
  101. }
  102. /**
  103. * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  104. *
  105. * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
  106. */
  107. createDocumentFragment() {
  108. return new DocumentFragment();
  109. }
  110. /**
  111. * Inserts item on given position.
  112. *
  113. * const paragraph = writer.createElement( 'paragraph' );
  114. * writer.insert( paragraph, position );
  115. *
  116. * Instead of using position you can use parent and offset:
  117. *
  118. * const text = writer.createText( 'foo' );
  119. * writer.insert( text, paragraph, 5 );
  120. *
  121. * You can also use `end` instead of the offset to insert at the end:
  122. *
  123. * const text = writer.createText( 'foo' );
  124. * writer.insert( text, paragraph, 'end' );
  125. *
  126. * Or insert before or after another element:
  127. *
  128. * const paragraph = writer.createElement( 'paragraph' );
  129. * writer.insert( paragraph, anotherParagraph, 'after' );
  130. *
  131. * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
  132. *
  133. * Note that if the item already has parent it will be removed from the previous parent.
  134. *
  135. * If you want to move {@link module:engine/model/range~Range range} instead of an
  136. * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move move}.
  137. *
  138. * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} item Item or document
  139. * fragment to insert.
  140. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  141. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  142. * second parameter is a {@link module:engine/model/item~Item model item}.
  143. */
  144. insert( item, itemOrPosition, offset ) {
  145. this._assertWriterUsedCorrectly();
  146. const position = Position.createAt( itemOrPosition, offset );
  147. // For text that has no parent we need to make a WeakInsert.
  148. const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
  149. // If item has a parent already.
  150. if ( item.parent ) {
  151. // We need to check if item is going to be inserted within the same document.
  152. if ( isSameTree( item.root, position.root ) ) {
  153. // If it's we just need to move it.
  154. this.move( Range.createOn( item ), position );
  155. return;
  156. }
  157. // If it isn't the same root.
  158. else {
  159. // We need to remove this item from old position first.
  160. this.remove( item );
  161. }
  162. }
  163. const version = position.root.document ? position.root.document.version : null;
  164. const insert = new InsertOperation( position, item, version );
  165. this.batch.addDelta( delta );
  166. delta.addOperation( insert );
  167. this.model.applyOperation( insert );
  168. // When element is a DocumentFragment we need to move its markers to Document#markers.
  169. if ( item instanceof DocumentFragment ) {
  170. for ( const [ markerName, markerRange ] of item.markers ) {
  171. // We need to migrate marker range from DocumentFragment to Document.
  172. const rangeRootPosition = Position.createAt( markerRange.root );
  173. const range = new Range(
  174. markerRange.start._getCombined( rangeRootPosition, position ),
  175. markerRange.end._getCombined( rangeRootPosition, position )
  176. );
  177. this.setMarker( markerName, range, { usingOperation: true } );
  178. }
  179. }
  180. }
  181. /**
  182. * Creates and inserts text on given position. You can optionally set text attributes:
  183. *
  184. * writer.insertText( 'foo', position );
  185. * writer.insertText( 'foo', { 'bold': true }, position );
  186. *
  187. * Instead of using position you can use parent and offset or define that text should be inserted at the end
  188. * or before or after other node:
  189. *
  190. * writer.insertText( 'foo', paragraph, 5 ); // inserts in paragraph, at offset 5
  191. * writer.insertText( 'foo', paragraph, 'end' ); // inserts at the end of the paragraph
  192. * writer.insertText( 'foo', image, 'after' ); // inserts after image
  193. *
  194. * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
  195. *
  196. * @param {String} data Text data.
  197. * @param {Object} [attributes] Text attributes.
  198. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  199. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  200. * third parameter is a {@link module:engine/model/item~Item model item}.
  201. */
  202. insertText( text, attributes, itemOrPosition, offset ) {
  203. if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
  204. this.insert( this.createText( text ), attributes, itemOrPosition );
  205. } else {
  206. this.insert( this.createText( text, attributes ), itemOrPosition, offset );
  207. }
  208. }
  209. /**
  210. * Creates and inserts element on given position. You can optionally set attributes:
  211. *
  212. * writer.insertElement( 'paragraph', position );
  213. * writer.insertElement( 'paragraph', { 'alignment': 'center' }, position );
  214. *
  215. * Instead of using position you can use parent and offset or define that text should be inserted at the end
  216. * or before or after other node:
  217. *
  218. * writer.insertElement( 'paragraph', paragraph, 5 ); // inserts in paragraph, at offset 5
  219. * writer.insertElement( 'paragraph', blockquote, 'end' ); // insets at the end of the blockquote
  220. * writer.insertElement( 'paragraph', image, 'after' ); // inserts after image
  221. *
  222. * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
  223. *
  224. * @param {String} name Name of the element.
  225. * @param {Object} [attributes] Elements attributes.
  226. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  227. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  228. * third parameter is a {@link module:engine/model/item~Item model item}.
  229. */
  230. insertElement( name, attributes, itemOrPosition, offset ) {
  231. if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
  232. this.insert( this.createElement( name ), attributes, itemOrPosition );
  233. } else {
  234. this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
  235. }
  236. }
  237. /**
  238. * Inserts item at the end of the given parent.
  239. *
  240. * const paragraph = writer.createElement( 'paragraph' );
  241. * writer.append( paragraph, root );
  242. *
  243. * Note that if the item already has parent it will be removed from the previous parent.
  244. *
  245. * If you want to move {@link module:engine/model/range~Range range} instead of an
  246. * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move move}.
  247. *
  248. * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
  249. * item Item or document fragment to insert.
  250. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  251. */
  252. append( item, parent ) {
  253. this.insert( item, parent, 'end' );
  254. }
  255. /**
  256. * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
  257. *
  258. * writer.appendText( 'foo', paragraph );
  259. * writer.appendText( 'foo', { 'bold': true }, paragraph );
  260. *
  261. * @param {String} text Text data.
  262. * @param {Object} [attributes] Text attributes.
  263. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  264. */
  265. appendText( text, attributes, parent ) {
  266. if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
  267. this.insert( this.createText( text ), attributes, 'end' );
  268. } else {
  269. this.insert( this.createText( text, attributes ), parent, 'end' );
  270. }
  271. }
  272. /**
  273. * Creates element and inserts it at the end of the parent. You can optionally set attributes:
  274. *
  275. * writer.appendElement( 'paragraph', root );
  276. * writer.appendElement( 'paragraph', { 'alignment': 'center' }, root );
  277. *
  278. * @param {String} name Name of the element.
  279. * @param {Object} [attributes] Elements attributes.
  280. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  281. */
  282. appendElement( name, attributes, parent ) {
  283. if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
  284. this.insert( this.createElement( name ), attributes, 'end' );
  285. } else {
  286. this.insert( this.createElement( name, attributes ), parent, 'end' );
  287. }
  288. }
  289. /**
  290. * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
  291. * or on a {@link module:engine/model/range~Range range}.
  292. *
  293. * @param {String} key Attribute key.
  294. * @param {*} value Attribute new value.
  295. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  296. * Model item or range on which the attribute will be set.
  297. */
  298. setAttribute( key, value, itemOrRange ) {
  299. this._assertWriterUsedCorrectly();
  300. if ( itemOrRange instanceof Range ) {
  301. setAttributeOnRange( this, key, value, itemOrRange );
  302. } else {
  303. setAttributeOnItem( this, key, value, itemOrRange );
  304. }
  305. }
  306. /**
  307. * Sets values of attributes on a {@link module:engine/model/item~Item model item}
  308. * or on a {@link module:engine/model/range~Range range}.
  309. *
  310. * writer.setAttributes( {
  311. * 'bold': true,
  312. * 'italic': true
  313. * }, range );
  314. *
  315. * @param {Object} attributes Attributes keys and values.
  316. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  317. * Model item or range on which the attributes will be set.
  318. */
  319. setAttributes( attributes, itemOrRange ) {
  320. for ( const [ key, val ] of toMap( attributes ) ) {
  321. this.setAttribute( key, val, itemOrRange );
  322. }
  323. }
  324. /**
  325. * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
  326. * or from a {@link module:engine/model/range~Range range}.
  327. *
  328. * @param {String} key Attribute key.
  329. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  330. * Model item or range from which the attribute will be removed.
  331. */
  332. removeAttribute( key, itemOrRange ) {
  333. this._assertWriterUsedCorrectly();
  334. if ( itemOrRange instanceof Range ) {
  335. setAttributeOnRange( this, key, null, itemOrRange );
  336. } else {
  337. setAttributeOnItem( this, key, null, itemOrRange );
  338. }
  339. }
  340. /**
  341. * Removes all attributes from all elements in the range or from the given item.
  342. *
  343. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  344. * Model item or range from which all attributes will be removed.
  345. */
  346. clearAttributes( itemOrRange ) {
  347. this._assertWriterUsedCorrectly();
  348. const removeAttributesFromItem = item => {
  349. for ( const attribute of item.getAttributeKeys() ) {
  350. this.removeAttribute( attribute, item );
  351. }
  352. };
  353. if ( !( itemOrRange instanceof Range ) ) {
  354. removeAttributesFromItem( itemOrRange );
  355. } else {
  356. for ( const item of itemOrRange.getItems() ) {
  357. removeAttributesFromItem( item );
  358. }
  359. }
  360. }
  361. /**
  362. * Moves all items in the source range to the target position.
  363. *
  364. * writer.move( sourceRange, targetPosition );
  365. *
  366. * Instead of the target position you can use parent and offset or define that range should be moved to the end
  367. * or before or after chosen item:
  368. *
  369. * writer.move( sourceRange, paragraph, 5 ); // moves all items in the range to the paragraph at offset 5
  370. * writer.move( sourceRange, blockquote, 'end' ); // moves all items in the range at the end of the blockquote
  371. * writer.move( sourceRange, image, 'after' ); // moves all items in the range after the image
  372. *
  373. * These parameters works the same way as {@link module:engine/model/position~Position.createAt}.
  374. *
  375. * Note that items can be moved only within the same tree. It means that you can move items within the same root
  376. * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
  377. * but you can not move items from document fragment to the document or from one detached element to another. Use
  378. * {@link module:engine/model/writer~Writer#insert} in such cases.
  379. *
  380. * @param {module:engine/model/range~Range} range Source range.
  381. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  382. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  383. * second parameter is a {@link module:engine/model/item~Item model item}.
  384. */
  385. move( range, itemOrPosition, offset ) {
  386. this._assertWriterUsedCorrectly();
  387. if ( !( range instanceof Range ) ) {
  388. /**
  389. * Invalid range to move.
  390. *
  391. * @error writer-move-invalid-range
  392. */
  393. throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.' );
  394. }
  395. if ( !range.isFlat ) {
  396. /**
  397. * Range to move is not flat.
  398. *
  399. * @error writer-move-range-not-flat
  400. */
  401. throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.' );
  402. }
  403. const position = Position.createAt( itemOrPosition, offset );
  404. if ( !isSameTree( range.root, position.root ) ) {
  405. /**
  406. * Range is going to be moved within not the same document. Please use
  407. * {@link module:engine/model/writer~Writer#insert insert} instead.
  408. *
  409. * @error writer-move-different-document
  410. */
  411. throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.' );
  412. }
  413. const delta = new MoveDelta();
  414. this.batch.addDelta( delta );
  415. const version = range.root.document ? range.root.document.version : null;
  416. const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, version );
  417. delta.addOperation( operation );
  418. this.model.applyOperation( operation );
  419. }
  420. /**
  421. * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
  422. *
  423. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
  424. */
  425. remove( itemOrRange ) {
  426. this._assertWriterUsedCorrectly();
  427. const addRemoveDelta = ( position, howMany ) => {
  428. const delta = new RemoveDelta();
  429. this.batch.addDelta( delta );
  430. applyRemoveOperation( position, howMany, delta, this.model );
  431. };
  432. if ( itemOrRange instanceof Range ) {
  433. // The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
  434. const ranges = itemOrRange.getMinimalFlatRanges().reverse();
  435. for ( const flat of ranges ) {
  436. addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
  437. }
  438. } else {
  439. const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
  440. addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
  441. }
  442. }
  443. /**
  444. * Merges two siblings at the given position.
  445. *
  446. * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
  447. * `writer-merge-no-element-after` error will be thrown.
  448. *
  449. * @param {module:engine/model/position~Position} position Position of merge.
  450. */
  451. merge( position ) {
  452. this._assertWriterUsedCorrectly();
  453. const delta = new MergeDelta();
  454. this.batch.addDelta( delta );
  455. const nodeBefore = position.nodeBefore;
  456. const nodeAfter = position.nodeAfter;
  457. if ( !( nodeBefore instanceof Element ) ) {
  458. /**
  459. * Node before merge position must be an element.
  460. *
  461. * @error writer-merge-no-element-before
  462. */
  463. throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.' );
  464. }
  465. if ( !( nodeAfter instanceof Element ) ) {
  466. /**
  467. * Node after merge position must be an element.
  468. *
  469. * @error writer-merge-no-element-after
  470. */
  471. throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.' );
  472. }
  473. const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
  474. const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
  475. const moveVersion = position.root.document ? position.root.document.version : null;
  476. const move = new MoveOperation(
  477. positionAfter,
  478. nodeAfter.maxOffset,
  479. positionBefore,
  480. moveVersion
  481. );
  482. move.isSticky = true;
  483. delta.addOperation( move );
  484. this.model.applyOperation( move );
  485. applyRemoveOperation( position, 1, delta, this.model );
  486. }
  487. /**
  488. * Renames given element.
  489. *
  490. * @param {module:engine/model/element~Element} element The element to rename.
  491. * @param {String} newName New element name.
  492. */
  493. rename( element, newName ) {
  494. this._assertWriterUsedCorrectly();
  495. if ( !( element instanceof Element ) ) {
  496. /**
  497. * Trying to rename an object which is not an instance of Element.
  498. *
  499. * @error writer-rename-not-element-instance
  500. */
  501. throw new CKEditorError(
  502. 'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.'
  503. );
  504. }
  505. const delta = new RenameDelta();
  506. this.batch.addDelta( delta );
  507. const version = element.root.document ? element.root.document.version : null;
  508. const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, version );
  509. delta.addOperation( renameOperation );
  510. this.model.applyOperation( renameOperation );
  511. }
  512. /**
  513. * Splits elements start from the given position and goes to the top of the model tree as long as given
  514. * `limitElement` won't be reached. When limitElement is not defined then only a parent of given position will be split.
  515. *
  516. * The element needs to have a parent. It cannot be a root element nor document fragment.
  517. * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
  518. *
  519. * @param {module:engine/model/position~Position} position Position of split.
  520. * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
  521. * @returns {Object} result Split result.
  522. * @returns {module:engine/model/position~Position} result.position between split elements.
  523. * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ands
  524. * at the beginning of the first copy element.
  525. */
  526. split( position, limitElement ) {
  527. this._assertWriterUsedCorrectly();
  528. let splitElement = position.parent;
  529. if ( !splitElement.parent ) {
  530. /**
  531. * Element with no parent can not be split.
  532. *
  533. * @error writer-split-element-no-parent
  534. */
  535. throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.' );
  536. }
  537. // When limit element is not defined lets set splitElement parent as limit.
  538. if ( !limitElement ) {
  539. limitElement = splitElement.parent;
  540. }
  541. if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
  542. throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.' );
  543. }
  544. // We need to cache elements that will be created as a result of the first split because
  545. // we need to create a range from the end of the first split element to the beginning of the
  546. // first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
  547. let firstSplitElement, firstCopyElement;
  548. do {
  549. const delta = new SplitDelta();
  550. this.batch.addDelta( delta );
  551. const copy = new Element( splitElement.name, splitElement.getAttributes() );
  552. const insertVersion = splitElement.root.document ? splitElement.root.document.version : null;
  553. const insert = new InsertOperation(
  554. Position.createAfter( splitElement ),
  555. copy,
  556. insertVersion
  557. );
  558. delta.addOperation( insert );
  559. this.model.applyOperation( insert );
  560. const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
  561. const move = new MoveOperation(
  562. position,
  563. splitElement.maxOffset - position.offset,
  564. Position.createFromParentAndOffset( copy, 0 ),
  565. moveVersion
  566. );
  567. move.isSticky = true;
  568. delta.addOperation( move );
  569. this.model.applyOperation( move );
  570. // Cache result of the first split.
  571. if ( !firstSplitElement && !firstCopyElement ) {
  572. firstSplitElement = splitElement;
  573. firstCopyElement = copy;
  574. }
  575. position = Position.createBefore( copy );
  576. splitElement = position.parent;
  577. } while ( splitElement !== limitElement );
  578. return {
  579. position,
  580. range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement ) )
  581. };
  582. }
  583. /**
  584. * Wraps given range with given element or with a new element with specified name, if string has been passed.
  585. *
  586. * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
  587. *
  588. * @param {module:engine/model/range~Range} range Range to wrap.
  589. * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
  590. */
  591. wrap( range, elementOrString ) {
  592. this._assertWriterUsedCorrectly();
  593. if ( !range.isFlat ) {
  594. /**
  595. * Range to wrap is not flat.
  596. *
  597. * @error writer-wrap-range-not-flat
  598. */
  599. throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.' );
  600. }
  601. const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
  602. if ( element.childCount > 0 ) {
  603. /**
  604. * Element to wrap with is not empty.
  605. *
  606. * @error writer-wrap-element-not-empty
  607. */
  608. throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.' );
  609. }
  610. if ( element.parent !== null ) {
  611. /**
  612. * Element to wrap with is already attached to a tree model.
  613. *
  614. * @error writer-wrap-element-attached
  615. */
  616. throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
  617. }
  618. const delta = new WrapDelta();
  619. this.batch.addDelta( delta );
  620. const insertVersion = range.root.document ? range.root.document.version : null;
  621. const insert = new InsertOperation( range.end, element, insertVersion );
  622. delta.addOperation( insert );
  623. this.model.applyOperation( insert );
  624. const moveVersion = insertVersion !== null ? insertVersion + 1 : null;
  625. const targetPosition = Position.createFromParentAndOffset( element, 0 );
  626. const move = new MoveOperation(
  627. range.start,
  628. range.end.offset - range.start.offset,
  629. targetPosition,
  630. moveVersion
  631. );
  632. delta.addOperation( move );
  633. this.model.applyOperation( move );
  634. }
  635. /**
  636. * Unwraps children of the given element – all its children are moved before it and then the element is removed.
  637. * Throws error if you try to unwrap an element which does not have a parent.
  638. *
  639. * @param {module:engine/model/element~Element} element Element to unwrap.
  640. */
  641. unwrap( element ) {
  642. this._assertWriterUsedCorrectly();
  643. if ( element.parent === null ) {
  644. /**
  645. * Trying to unwrap an element which has no parent.
  646. *
  647. * @error writer-unwrap-element-no-parent
  648. */
  649. throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
  650. }
  651. const delta = new UnwrapDelta();
  652. this.batch.addDelta( delta );
  653. const sourcePosition = Position.createFromParentAndOffset( element, 0 );
  654. const moveVersion = sourcePosition.root.document ? sourcePosition.root.document.version : null;
  655. const move = new MoveOperation(
  656. sourcePosition,
  657. element.maxOffset,
  658. Position.createBefore( element ),
  659. moveVersion
  660. );
  661. move.isSticky = true;
  662. delta.addOperation( move );
  663. this.model.applyOperation( move );
  664. applyRemoveOperation( Position.createBefore( element ), 1, delta, this.model );
  665. }
  666. /**
  667. * Adds or updates {@link module:engine/model/markercollection~Marker marker}.
  668. *
  669. * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
  670. * name is created and returned.
  671. *
  672. * Using this method you can change markers range or define if the marker is managed by operation or not.
  673. *
  674. * Marker tracks changes is the document and updates the range automatically, so you need to update the range only
  675. * when it changes directly. You do not need to update it after each document change.
  676. *
  677. * The option parameter let you decide if the marker should be managed by operations or not. See
  678. * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
  679. * markers managed by operation and managed directly. You can change this option for existing marker. This is
  680. * useful if a marker have been created earlier and need to be added to the document history later.
  681. *
  682. * Update marker using operation:
  683. *
  684. * setMarker( marker, range, { usingOperation: true } );
  685. *
  686. * Create/update marker directly base on marker's name:
  687. *
  688. * setMarker( markerName, range );
  689. *
  690. * Create marker with a unique id using operation:
  691. *
  692. * setMarker( range, { usingOperation: true } );
  693. *
  694. * Create marker directly with a unique name:
  695. *
  696. * setMarker( range )
  697. *
  698. * Change marker's option (start using operations to manage it):
  699. *
  700. * setMarker( marker, { usingOperation: true } );
  701. *
  702. * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
  703. *
  704. * @see module:engine/model/markercollection~Marker
  705. * @param {module:engine/model/markercollection~Marker|String} [markerOrName=uid()]
  706. * Name of marker to add, Marker instance to update or range for the marker with a unique name.
  707. * @param {module:engine/model/range~Range|Object} [range] Marker range or options.
  708. * @param {Object} [options]
  709. * @param {Boolean} [options.usingOperation=false] Flag indicated whether the marker should be added by MarkerOperation.
  710. * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
  711. * @returns {module:engine/model/markercollection~Marker} Marker that was set.
  712. */
  713. setMarker( markerOrNameOrRange, rangeOrOptions, options ) {
  714. this._assertWriterUsedCorrectly();
  715. let markerName, newRange, usingOperation;
  716. if ( markerOrNameOrRange instanceof Range ) {
  717. markerName = uid();
  718. newRange = markerOrNameOrRange;
  719. usingOperation = !!rangeOrOptions && !!rangeOrOptions.usingOperation;
  720. } else {
  721. markerName = typeof markerOrNameOrRange === 'string' ? markerOrNameOrRange : markerOrNameOrRange.name;
  722. if ( rangeOrOptions instanceof Range ) {
  723. newRange = rangeOrOptions;
  724. usingOperation = !!options && !!options.usingOperation;
  725. } else {
  726. newRange = null;
  727. usingOperation = !!rangeOrOptions && !!rangeOrOptions.usingOperation;
  728. }
  729. }
  730. const currentMarker = this.model.markers.get( markerName );
  731. if ( !usingOperation ) {
  732. if ( !newRange ) {
  733. /**
  734. * Range parameter is required when adding a new marker.
  735. *
  736. * @error writer-setMarker-no-range
  737. */
  738. throw new CKEditorError( 'writer-setMarker-no-range: Range parameter is required when adding a new marker.' );
  739. }
  740. // If marker changes to marker that do not use operations then we need to create additional operation
  741. // that removes that marker first.
  742. if ( currentMarker && currentMarker.managedUsingOperations && !usingOperation ) {
  743. applyMarkerOperation( this, markerName, currentMarker.getRange(), null );
  744. }
  745. return this.model.markers._set( markerName, newRange, usingOperation );
  746. }
  747. if ( !newRange && !currentMarker ) {
  748. throw new CKEditorError( 'writer-setMarker-no-range: Range parameter is required when adding a new marker.' );
  749. }
  750. const currentRange = currentMarker ? currentMarker.getRange() : null;
  751. if ( !newRange ) {
  752. // If `newRange` is not given, treat this as synchronizing existing marker.
  753. // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
  754. applyMarkerOperation( this, markerName, null, currentRange );
  755. } else {
  756. // Just change marker range.
  757. applyMarkerOperation( this, markerName, currentRange, newRange );
  758. }
  759. return this.model.markers.get( markerName );
  760. }
  761. /**
  762. * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
  763. * The marker is removed accordingly to how it has been created, so if the marker was created using operation,
  764. * it will be destroyed using operation.
  765. *
  766. * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
  767. */
  768. removeMarker( markerOrName ) {
  769. this._assertWriterUsedCorrectly();
  770. const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
  771. if ( !this.model.markers.has( name ) ) {
  772. /**
  773. * Trying to remove marker which does not exist.
  774. *
  775. * @error writer-removeMarker-no-marker
  776. */
  777. throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.' );
  778. }
  779. const marker = this.model.markers.get( name );
  780. if ( !marker.managedUsingOperations ) {
  781. this.model.markers._remove( name );
  782. return;
  783. }
  784. const oldRange = marker.getRange();
  785. applyMarkerOperation( this, name, oldRange, null );
  786. }
  787. /**
  788. * Sets this selection's ranges and direction to the specified location based on the given
  789. * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
  790. * {@link module:engine/model/element~Element element}, {@link module:engine/model/position~Position position},
  791. * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
  792. *
  793. * // Sets ranges from the given range.
  794. * const range = new Range( start, end );
  795. * writer.setSelection( range, isBackwardSelection );
  796. *
  797. * // Sets ranges from the iterable of ranges.
  798. * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
  799. * writer.setSelection( range, isBackwardSelection );
  800. *
  801. * // Sets ranges from the other selection.
  802. * const otherSelection = new Selection();
  803. * writer.setSelection( otherSelection );
  804. *
  805. * // Sets ranges from the given document selection's ranges.
  806. * const documentSelection = new DocumentSelection( doc );
  807. * writer.setSelection( documentSelection );
  808. *
  809. * // Sets collapsed range at the given position.
  810. * const position = new Position( root, path );
  811. * writer.setSelection( position );
  812. *
  813. * // Sets collapsed range at the given offset in element.
  814. * const paragraph = writer.createElement( 'paragraph' );
  815. * writer.setSelection( paragraph, offset );
  816. *
  817. * // Removes all ranges.
  818. * writer.setSelection( null );
  819. *
  820. * Throws `writer-incorrect-use` error when the writer is used outside the `change()` block.
  821. *
  822. * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
  823. * module:engine/model/position~Position|module:engine/model/element~Element|
  824. * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
  825. * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset]
  826. */
  827. setSelection( selectable, backwardSelectionOrOffset ) {
  828. this._assertWriterUsedCorrectly();
  829. this.model.document.selection._setTo( selectable, backwardSelectionOrOffset );
  830. }
  831. /**
  832. * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
  833. *
  834. * The location can be specified in the same form as {@link module:engine/model/position~Position.createAt} parameters.
  835. *
  836. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  837. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  838. * first parameter is a {@link module:engine/model/item~Item model item}.
  839. */
  840. setSelectionFocus( itemOrPosition, offset ) {
  841. this._assertWriterUsedCorrectly();
  842. this.model.document.selection._setFocus( itemOrPosition, offset );
  843. }
  844. /**
  845. * Sets attribute(s) on the selection. If attribute with the same key already is set, it's value is overwritten.
  846. *
  847. * Using key and value pair:
  848. *
  849. * writer.setSelectionAttribute( 'italic', true );
  850. *
  851. * Using key-value object:
  852. *
  853. * writer.setSelectionAttribute( { italic: true, bold: false } );
  854. *
  855. * Using iterable object:
  856. *
  857. * writer.setSelectionAttribute( new Map( [ [ 'italic', true ] ] ) );
  858. *
  859. * @param {String|Object|Iterable.<*>} keyOrObjectOrIterable Key of the attribute to set
  860. * or object / iterable of key - value attribute pairs.
  861. * @param {*} [value] Attribute value.
  862. */
  863. setSelectionAttribute( keyOrObjectOrIterable, value ) {
  864. this._assertWriterUsedCorrectly();
  865. if ( typeof keyOrObjectOrIterable === 'string' ) {
  866. this._setSelectionAttribute( keyOrObjectOrIterable, value );
  867. } else {
  868. for ( const [ key, value ] of toMap( keyOrObjectOrIterable ) ) {
  869. this._setSelectionAttribute( key, value );
  870. }
  871. }
  872. }
  873. /**
  874. * Removes attribute(s) with given key(s) from the selection.
  875. *
  876. * Using key
  877. *
  878. * writer.removeSelectionAttribute( 'italic' );
  879. *
  880. * Using iterable of keys
  881. *
  882. * writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
  883. *
  884. * @param {String|Iterable.<String>} keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
  885. */
  886. removeSelectionAttribute( keyOrIterableOfKeys ) {
  887. this._assertWriterUsedCorrectly();
  888. if ( typeof keyOrIterableOfKeys === 'string' ) {
  889. this._removeSelectionAttribute( keyOrIterableOfKeys );
  890. } else {
  891. for ( const key of keyOrIterableOfKeys ) {
  892. this._removeSelectionAttribute( key );
  893. }
  894. }
  895. }
  896. /**
  897. * @private
  898. * @param {String} key Key of the attribute to remove.
  899. * @param {*} value Attribute value.
  900. */
  901. _setSelectionAttribute( key, value ) {
  902. const selection = this.model.document.selection;
  903. // Store attribute in parent element if the selection is collapsed in an empty node.
  904. if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
  905. const storeKey = DocumentSelection._getStoreAttributeKey( key );
  906. this.setAttribute( storeKey, value, selection.anchor.parent );
  907. }
  908. selection._setAttribute( key, value );
  909. }
  910. /**
  911. * @private
  912. * @param {String} key Key of the attribute to remove.
  913. */
  914. _removeSelectionAttribute( key ) {
  915. const selection = this.model.document.selection;
  916. // Remove stored attribute from parent element if the selection is collapsed in an empty node.
  917. if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
  918. const storeKey = DocumentSelection._getStoreAttributeKey( key );
  919. this.removeAttribute( storeKey, selection.anchor.parent );
  920. }
  921. selection._removeAttribute( key );
  922. }
  923. /**
  924. * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
  925. *
  926. * @private
  927. */
  928. _assertWriterUsedCorrectly() {
  929. /**
  930. * Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()` or
  931. * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks.
  932. *
  933. * The writer can only be used inside these blocks which ensures that the model
  934. * can only be changed during such "sessions".
  935. *
  936. * @error writer-incorrect-use
  937. */
  938. if ( this.model._currentWriter !== this ) {
  939. throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' );
  940. }
  941. }
  942. }
  943. // Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
  944. //
  945. // Because attribute operation needs to have the same attribute value on the whole range, this function splits
  946. // the range into smaller parts.
  947. //
  948. // @private
  949. // @param {module:engine/model/writer~Writer} writer
  950. // @param {String} key Attribute key.
  951. // @param {*} value Attribute new value.
  952. // @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
  953. function setAttributeOnRange( writer, key, value, range ) {
  954. const delta = new AttributeDelta();
  955. const model = writer.model;
  956. const doc = model.document;
  957. // Position of the last split, the beginning of the new range.
  958. let lastSplitPosition = range.start;
  959. // Currently position in the scanning range. Because we need value after the position, it is not a current
  960. // position of the iterator but the previous one (we need to iterate one more time to get the value after).
  961. let position;
  962. // Value before the currently position.
  963. let valueBefore;
  964. // Value after the currently position.
  965. let valueAfter;
  966. for ( const val of range ) {
  967. valueAfter = val.item.getAttribute( key );
  968. // At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
  969. // because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
  970. if ( position && valueBefore != valueAfter ) {
  971. // if valueBefore == value there is nothing to change, so we add operation only if these values are different.
  972. if ( valueBefore != value ) {
  973. addOperation();
  974. }
  975. lastSplitPosition = position;
  976. }
  977. position = val.nextPosition;
  978. valueBefore = valueAfter;
  979. }
  980. // Because position in the loop is not the iterator position (see let position comment), the last position in
  981. // the while loop will be last but one position in the range. We need to check the last position manually.
  982. if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
  983. addOperation();
  984. }
  985. function addOperation() {
  986. // Add delta to the batch only if there is at least operation in the delta. Add delta only once.
  987. if ( delta.operations.length === 0 ) {
  988. writer.batch.addDelta( delta );
  989. }
  990. const range = new Range( lastSplitPosition, position );
  991. const version = range.root.document ? doc.version : null;
  992. const operation = new AttributeOperation( range, key, valueBefore, value, version );
  993. delta.addOperation( operation );
  994. model.applyOperation( operation );
  995. }
  996. }
  997. // Sets given attribute to the given node. When attribute value is null then attribute will be removed.
  998. //
  999. // @private
  1000. // @param {module:engine/model/writer~Writer} writer
  1001. // @param {String} key Attribute key.
  1002. // @param {*} value Attribute new value.
  1003. // @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
  1004. function setAttributeOnItem( writer, key, value, item ) {
  1005. const model = writer.model;
  1006. const doc = model.document;
  1007. const previousValue = item.getAttribute( key );
  1008. let range, operation;
  1009. if ( previousValue != value ) {
  1010. const isRootChanged = item.root === item;
  1011. const delta = isRootChanged ? new RootAttributeDelta() : new AttributeDelta();
  1012. writer.batch.addDelta( delta );
  1013. if ( isRootChanged ) {
  1014. // If we change attributes of root element, we have to use `RootAttributeOperation`.
  1015. const version = item.document ? doc.version : null;
  1016. operation = new RootAttributeOperation( item, key, previousValue, value, version );
  1017. } else {
  1018. if ( item.is( 'element' ) ) {
  1019. // If we change the attribute of the element, we do not want to change attributes of its children, so
  1020. // the end of the range cannot be after the closing tag, it should be inside that element, before any of
  1021. // it's children, so the range will contain only the opening tag.
  1022. range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
  1023. } else {
  1024. // If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
  1025. // all characters represented by it.
  1026. range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
  1027. }
  1028. const version = range.root.document ? doc.version : null;
  1029. operation = new AttributeOperation( range, key, previousValue, value, version );
  1030. }
  1031. delta.addOperation( operation );
  1032. model.applyOperation( operation );
  1033. }
  1034. }
  1035. // Creates and applies marker operation to {@link module:engine/model/delta/delta~Delta delta}.
  1036. //
  1037. // @private
  1038. // @param {module:engine/model/writer~Writer} writer
  1039. // @param {String} name Marker name.
  1040. // @param {module:engine/model/range~Range} oldRange Marker range before the change.
  1041. // @param {module:engine/model/range~Range} newRange Marker range after the change.
  1042. function applyMarkerOperation( writer, name, oldRange, newRange ) {
  1043. const model = writer.model;
  1044. const doc = model.document;
  1045. const delta = new MarkerDelta();
  1046. const operation = new MarkerOperation( name, oldRange, newRange, model.markers, doc.version );
  1047. writer.batch.addDelta( delta );
  1048. delta.addOperation( operation );
  1049. model.applyOperation( operation );
  1050. }
  1051. // Creates `RemoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
  1052. // The operation will be applied on given model instance and added to given delta instance.
  1053. //
  1054. // @private
  1055. // @param {module:engine/model/position~Position} position Position from which nodes are removed.
  1056. // @param {Number} howMany Number of nodes to remove.
  1057. // @param {module:engine/model/delta~Delta} delta Delta to add new operation to.
  1058. // @param {module:engine/model/model~Model} model Model instance on which operation will be applied.
  1059. function applyRemoveOperation( position, howMany, delta, model ) {
  1060. let operation;
  1061. if ( position.root.document ) {
  1062. const doc = model.document;
  1063. const graveyardPosition = new Position( doc.graveyard, [ 0 ] );
  1064. operation = new RemoveOperation( position, howMany, graveyardPosition, doc.version );
  1065. } else {
  1066. operation = new DetachOperation( position, howMany );
  1067. }
  1068. delta.addOperation( operation );
  1069. model.applyOperation( operation );
  1070. }
  1071. // Returns `true` if both root elements are the same element or both are documents root elements.
  1072. //
  1073. // Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
  1074. // within the same document fragment), but when element supposed to be moved from document fragment to the document, or
  1075. // to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
  1076. // collaboration may track changes on the document but ignore changes on detached fragments and should not get
  1077. // unexpected `move` operation.
  1078. function isSameTree( rootA, rootB ) {
  1079. // If it is the same root this is the same tree.
  1080. if ( rootA === rootB ) {
  1081. return true;
  1082. }
  1083. // If both roots are documents root it is operation within the document what we still treat as the same tree.
  1084. if ( rootA instanceof RootElement && rootB instanceof RootElement ) {
  1085. return true;
  1086. }
  1087. return false;
  1088. }