8
0

batch.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module engine/model/batch
  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 toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  35. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  36. /**
  37. * `Batch` instance groups document changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single `Batch`
  38. * can be reverted together, so you can think about `Batch` as of a single undo step. If you want to extend given undo step you
  39. * can call another method on the same `Batch` object. If you want to create a separate undo step you can create a new `Batch`.
  40. *
  41. * For example to create two separate undo steps you can call:
  42. *
  43. * doc.batch().insert( firstPosition, 'foo' );
  44. * doc.batch().insert( secondPosition, 'bar' );
  45. *
  46. * To create a single undo step:
  47. *
  48. * const batch = doc.batch();
  49. * batch.insert( firstPosition, 'foo' );
  50. * batch.insert( secondPosition, 'bar' );
  51. *
  52. */
  53. export default class Batch {
  54. /**
  55. * Creates `Batch` instance. Not recommended to use directly, use {@link module:engine/model/document~Document#batch} instead.
  56. *
  57. * @param {module:engine/model/document~Document} document Document which this Batch changes.
  58. * @param {'transparent'|'default'} [type='default'] Type of the batch.
  59. */
  60. constructor( document, type = 'default' ) {
  61. /**
  62. * Document which this batch changes.
  63. *
  64. * @readonly
  65. * @member {module:engine/model/document~Document} module:engine/model/batch~Batch#document
  66. */
  67. this.document = document;
  68. /**
  69. * Array of deltas which compose this batch.
  70. *
  71. * @readonly
  72. * @member {Array.<module:engine/model/delta/delta~Delta>} module:engine/model/batch~Batch#deltas
  73. */
  74. this.deltas = [];
  75. /**
  76. * Type of the batch.
  77. *
  78. * Can be one of the following values:
  79. * * `'default'` - all "normal" batches, most commonly used type.
  80. * * `'transparent'` - batch that should be ignored by other features, i.e. initial batch or collaborative editing changes.
  81. *
  82. * @readonly
  83. * @member {'transparent'|'default'} module:engine/model/batch~Batch#type
  84. */
  85. this.type = type;
  86. }
  87. /**
  88. * Returns this batch base version, which is equal to the base version of first delta in the batch.
  89. * If there are no deltas in the batch, it returns `null`.
  90. *
  91. * @readonly
  92. * @type {Number|null}
  93. */
  94. get baseVersion() {
  95. return this.deltas.length > 0 ? this.deltas[ 0 ].baseVersion : null;
  96. }
  97. /**
  98. * Adds delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method
  99. * to add created deltas.
  100. *
  101. * @param {module:engine/model/delta/delta~Delta} delta Delta to add.
  102. * @return {module:engine/model/delta/delta~Delta} Added delta.
  103. */
  104. addDelta( delta ) {
  105. delta.batch = this;
  106. this.deltas.push( delta );
  107. return delta;
  108. }
  109. /**
  110. * Gets an iterable collection of operations.
  111. *
  112. * @returns {Iterable.<module:engine/model/operation/operation~Operation>}
  113. */
  114. * getOperations() {
  115. for ( const delta of this.deltas ) {
  116. yield* delta.operations;
  117. }
  118. }
  119. /**
  120. * Creates a new {@link module:engine/model/text~Text text node}.
  121. *
  122. * batch.createText( 'foo' );
  123. * batch.createText( 'foo', { bold: true } );
  124. *
  125. * @param {String} data Text data.
  126. * @param {Object} [attributes] Text attributes.
  127. * @returns {module:engine/model/text~Text} Created text node.
  128. */
  129. createText( data, attributes ) {
  130. return new Text( data, attributes );
  131. }
  132. /**
  133. * Creates a new {@link module:engine/model/element~Element element}.
  134. *
  135. * batch.createElement( 'paragraph' );
  136. * batch.createElement( 'paragraph', { 'alignment': 'center' } );
  137. *
  138. * @param {String} name Name of the element.
  139. * @param {Object} [attributes] Elements attributes.
  140. * @returns {module:engine/model/element~Element} Created element.
  141. */
  142. createElement( name, attributes ) {
  143. return new Element( name, attributes );
  144. }
  145. /**
  146. * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  147. *
  148. * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
  149. */
  150. createDocumentFragment() {
  151. return new DocumentFragment();
  152. }
  153. /**
  154. * Inserts item on given position.
  155. *
  156. * const paragraph = batch.createElement( 'paragraph' );
  157. * batch.insert( paragraph, position );
  158. *
  159. * Instead of using position you can use parent and offset:
  160. *
  161. * const text = batch.createText( 'foo' );
  162. * batch.insert( text, paragraph, 5 );
  163. *
  164. * You can also use 'end' instead of the offset to insert at the end:
  165. *
  166. * const text = batch.createText( 'foo' );
  167. * batch.insert( text, paragraph, 'end' );
  168. *
  169. * Or insert before or after another element:
  170. *
  171. * const anotherParagraph = batch.createElement( 'paragraph' );
  172. * batch.insert( anotherParagraph, paragraph, 'after' );
  173. *
  174. * These parameters works the same way as {@link module:engine/model/position~Position#createAt}.
  175. *
  176. * Note that if the item already has parent it will be removed from the previous parent.
  177. *
  178. * If you want to move {@link module:engine/model/range~Range range} instead of an
  179. * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move batch}.
  180. *
  181. * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
  182. * item Item or document fragment to insert.
  183. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  184. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  185. * second parameter is a {@link module:engine/model/item~Item model item}.
  186. */
  187. insert( item, itemOrPosition, offset ) {
  188. const position = Position.createAt( itemOrPosition, offset );
  189. // For text that has no parent we need to make a WeakInsert.
  190. const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
  191. // If item has a parent already.
  192. if ( item.parent ) {
  193. // We need to check if item is going to be inserted within the same document.
  194. if ( isSameTree( item.root, position.root ) ) {
  195. // If it's we just need to move it.
  196. this.move( Range.createOn( item ), position );
  197. return;
  198. }
  199. // If it isn't the same root.
  200. else {
  201. // We need to remove this item from old position first.
  202. this.remove( item );
  203. }
  204. }
  205. const insert = new InsertOperation( position, item, this.document.version );
  206. this.addDelta( delta );
  207. delta.addOperation( insert );
  208. this.document.applyOperation( insert );
  209. // When element is a DocumentFragment we need to move its markers to Document#markers.
  210. if ( item instanceof DocumentFragment ) {
  211. for ( const [ markerName, markerRange ] of item.markers ) {
  212. // We need to migrate marker range from DocumentFragment to Document.
  213. const rangeRootPosition = Position.createAt( markerRange.root );
  214. const range = new Range(
  215. markerRange.start._getCombined( rangeRootPosition, position ),
  216. markerRange.end._getCombined( rangeRootPosition, position )
  217. );
  218. this.setMarker( markerName, range );
  219. }
  220. }
  221. }
  222. /**
  223. * Creates and inserts text on given position. You can optionally set text attributes:
  224. *
  225. * batch.insertText( 'foo', position );
  226. * batch.insertText( 'foo', { 'bold': true }, position );
  227. *
  228. * Instead of using position you can use parent and offset or define that text should be inserted at the end
  229. * or before or after other node:
  230. *
  231. * batch.insertText( 'foo', paragraph, 5 );
  232. * batch.insertText( 'foo', paragraph, 'end' ); // insets at the end of the paragraph
  233. * batch.insertText( 'foo', image, 'after' ); // inserts after image
  234. *
  235. * These parameters works the same way as {@link module:engine/model/position~Position#createAt}.
  236. *
  237. * @param {String} data Text data.
  238. * @param {Object} [attributes] Text attributes.
  239. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  240. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  241. * third parameter is a {@link module:engine/model/item~Item model item}.
  242. */
  243. insertText( text, attributes, itemOrPosition, offset ) {
  244. if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
  245. this.insert( this.createText( text ), attributes, itemOrPosition );
  246. } else {
  247. this.insert( this.createText( text, attributes ), itemOrPosition, offset );
  248. }
  249. }
  250. /**
  251. * Creates and inserts element on given position. You can optionally set attributes:
  252. *
  253. * batch.insertElement( 'paragraph', position );
  254. * batch.insertElement( 'paragraph', { 'alignment': 'center' }, position );
  255. *
  256. * Instead of using position you can use parent and offset or define that text should be inserted at the end
  257. * or before or after other node:
  258. *
  259. * batch.insertElement( 'paragraph', paragraph, 5 );
  260. * batch.insertElement( 'paragraph', blockquote, 'end' ); // insets at the end of the blockquote
  261. * batch.insertElement( 'paragraph', image, 'after' ); // inserts after image
  262. *
  263. * These parameters works the same way as {@link module:engine/model/position~Position#createAt}.
  264. *
  265. * @param {String} name Name of the element.
  266. * @param {Object} [attributes] Elements attributes.
  267. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  268. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  269. * third parameter is a {@link module:engine/model/item~Item model item}.
  270. */
  271. insertElement( name, attributes, itemOrPosition, offset ) {
  272. if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
  273. this.insert( this.createElement( name ), attributes, itemOrPosition );
  274. } else {
  275. this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
  276. }
  277. }
  278. /**
  279. * Inserts item at the end of the given parent.
  280. *
  281. * const paragraph = batch.createElement( 'paragraph' );
  282. * batch.append( paragraph, root );
  283. *
  284. * Note that if the item already has parent it will be removed from the previous parent.
  285. *
  286. * If you want to move {@link module:engine/model/range~Range range} instead of an
  287. * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move batch}.
  288. *
  289. * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
  290. * item Item or document fragment to insert.
  291. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  292. */
  293. append( item, parent ) {
  294. this.insert( item, parent, 'end' );
  295. }
  296. /**
  297. * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
  298. *
  299. * batch.appendText( 'foo', paragraph );
  300. * batch.appendText( 'foo', { 'bold': true }, paragraph );
  301. *
  302. * @param {String} text Text data.
  303. * @param {Object} [attributes] Text attributes.
  304. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  305. */
  306. appendText( text, attributes, parent ) {
  307. if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
  308. this.insert( this.createText( text ), attributes, 'end' );
  309. } else {
  310. this.insert( this.createText( text, attributes ), parent, 'end' );
  311. }
  312. }
  313. /**
  314. * Creates element and inserts it at the end of the parent. You can optionally set attributes:
  315. *
  316. * batch.appendElement( 'paragraph', root );
  317. * batch.appendElement( 'paragraph', { 'alignment': 'center' }, root );
  318. *
  319. * @param {String} name Name of the element.
  320. * @param {Object} [attributes] Elements attributes.
  321. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  322. */
  323. appendElement( name, attributes, parent ) {
  324. if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
  325. this.insert( this.createElement( name ), attributes, 'end' );
  326. } else {
  327. this.insert( this.createElement( name, attributes ), parent, 'end' );
  328. }
  329. }
  330. /**
  331. * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
  332. * or on a {@link module:engine/model/range~Range range}.
  333. *
  334. * @param {String} key Attribute key.
  335. * @param {*} value Attribute new value.
  336. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  337. * Model item or range on which the attribute will be set.
  338. */
  339. setAttribute( key, value, itemOrRange ) {
  340. if ( itemOrRange instanceof Range ) {
  341. setAttributeToRange( this, key, value, itemOrRange );
  342. } else {
  343. setAttributeToItem( this, key, value, itemOrRange );
  344. }
  345. }
  346. /**
  347. * Sets values of attributes on a {@link module:engine/model/item~Item model item}
  348. * or on a {@link module:engine/model/range~Range range}.
  349. *
  350. * batch.setAttributes( {
  351. * 'bold': true,
  352. * 'italic': true
  353. * }, range );
  354. *
  355. * @param {Object} attributes Attributes keys and values.
  356. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  357. * Model item or range on which the attributes will be set.
  358. */
  359. setAttributes( attributes, itemOrRange ) {
  360. for ( const [ key, val ] of toMap( attributes ) ) {
  361. this.setAttribute( key, val, itemOrRange );
  362. }
  363. }
  364. /**
  365. * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
  366. * or from a {@link module:engine/model/range~Range range}.
  367. *
  368. * @param {String} key Attribute key.
  369. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  370. * Model item or range from which the attribute will be removed.
  371. */
  372. removeAttribute( key, itemOrRange ) {
  373. if ( itemOrRange instanceof Range ) {
  374. setAttributeToRange( this, key, null, itemOrRange );
  375. } else {
  376. setAttributeToItem( this, key, null, itemOrRange );
  377. }
  378. }
  379. /**
  380. * Removes all attributes from all elements in the range or from the given item.
  381. *
  382. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  383. * Model item or range from which all attributes will be removed.
  384. */
  385. clearAttributes( itemOrRange ) {
  386. const removeAttributesFromItem = item => {
  387. for ( const attribute of item.getAttributeKeys() ) {
  388. this.removeAttribute( attribute, item );
  389. }
  390. };
  391. if ( !( itemOrRange instanceof Range ) ) {
  392. removeAttributesFromItem( itemOrRange );
  393. } else {
  394. for ( const item of itemOrRange.getItems() ) {
  395. removeAttributesFromItem( item );
  396. }
  397. }
  398. }
  399. /**
  400. * Moves all items in the source range to the target position.
  401. *
  402. * batch.move( sourceRange, targetPosition );
  403. *
  404. * Instead of the target position you can use parent and offset or define that range should be moved to the end
  405. * or before or after chosen item:
  406. *
  407. * batch.move( sourceRange, paragraph, 5 ); // moves all items in the range to the paragraph at offset 5
  408. * batch.move( sourceRange, blockquote, 'end' ); // moves all items in the range at the end of the blockquote
  409. * batch.move( sourceRange, image, 'after' ); // moves all items in the range after the image
  410. *
  411. * These parameters works the same way as {@link module:engine/model/position~Position#createAt}.
  412. *
  413. * Note that items can be moved only within the same tree. It means that you can move items within the same root
  414. * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
  415. * but you can not move items from document fragment to the document or from one detached element to another. Use
  416. * {@link module:engine/model/batch~Batch#insert} in such cases.
  417. *
  418. * @param {module:engine/model/range~Range} range Source range.
  419. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  420. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  421. * second parameter is a {@link module:engine/model/item~Item model item}.
  422. */
  423. move( range, itemOrPosition, offset ) {
  424. if ( !( range instanceof Range ) ) {
  425. /**
  426. * Invalid range to move.
  427. *
  428. * @error batch-move-invalid-range
  429. */
  430. throw new CKEditorError( 'batch-move-invalid-range: Invalid range to move.' );
  431. }
  432. if ( !range.isFlat ) {
  433. /**
  434. * Range to move is not flat.
  435. *
  436. * @error batch-move-range-not-flat
  437. */
  438. throw new CKEditorError( 'batch-move-range-not-flat: Range to move is not flat.' );
  439. }
  440. const position = Position.createAt( itemOrPosition, offset );
  441. if ( !isSameTree( range.root, position.root ) ) {
  442. /**
  443. * Range is going to be moved within not the same document. Please use
  444. * {@link module:engine/model/batch~Batch#insert insert} instead.
  445. *
  446. * @error batch-move-different-document
  447. */
  448. throw new CKEditorError( 'batch-move-different-document: Range is going to be moved between different documents.' );
  449. }
  450. const delta = new MoveDelta();
  451. this.addDelta( delta );
  452. const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, this.document.version );
  453. delta.addOperation( operation );
  454. this.document.applyOperation( operation );
  455. }
  456. /**
  457. * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
  458. *
  459. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
  460. */
  461. remove( itemOrRange ) {
  462. const addRemoveDelta = ( position, howMany ) => {
  463. const delta = new RemoveDelta();
  464. this.addDelta( delta );
  465. let operation;
  466. if ( position.root.document ) {
  467. const graveyard = this.document.graveyard;
  468. const gyPosition = new Position( graveyard, [ 0 ] );
  469. operation = new RemoveOperation( position, howMany, gyPosition, this.document.version );
  470. } else {
  471. operation = new DetachOperation( position, howMany, this.document.version );
  472. }
  473. delta.addOperation( operation );
  474. this.document.applyOperation( operation );
  475. };
  476. if ( itemOrRange instanceof Range ) {
  477. // The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
  478. const ranges = itemOrRange.getMinimalFlatRanges().reverse();
  479. for ( const flat of ranges ) {
  480. addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
  481. }
  482. } else {
  483. const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
  484. addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
  485. }
  486. }
  487. /**
  488. * Merges two siblings at the given position.
  489. *
  490. * Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or
  491. * `batch-merge-no-element-after` error will be thrown.
  492. *
  493. * @param {module:engine/model/position~Position} position Position of merge.
  494. */
  495. merge( position ) {
  496. const delta = new MergeDelta();
  497. this.addDelta( delta );
  498. const nodeBefore = position.nodeBefore;
  499. const nodeAfter = position.nodeAfter;
  500. if ( !( nodeBefore instanceof Element ) ) {
  501. /**
  502. * Node before merge position must be an element.
  503. *
  504. * @error batch-merge-no-element-before
  505. */
  506. throw new CKEditorError( 'batch-merge-no-element-before: Node before merge position must be an element.' );
  507. }
  508. if ( !( nodeAfter instanceof Element ) ) {
  509. /**
  510. * Node after merge position must be an element.
  511. *
  512. * @error batch-merge-no-element-after
  513. */
  514. throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
  515. }
  516. const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
  517. const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
  518. const move = new MoveOperation(
  519. positionAfter,
  520. nodeAfter.maxOffset,
  521. positionBefore,
  522. this.document.version
  523. );
  524. move.isSticky = true;
  525. delta.addOperation( move );
  526. this.document.applyOperation( move );
  527. const graveyard = this.document.graveyard;
  528. const gyPosition = new Position( graveyard, [ 0 ] );
  529. const remove = new RemoveOperation( position, 1, gyPosition, this.document.version );
  530. delta.addOperation( remove );
  531. this.document.applyOperation( remove );
  532. }
  533. /**
  534. * Renames given element.
  535. *
  536. * @param {module:engine/model/element~Element} element The element to rename.
  537. * @param {String} newName New element name.
  538. */
  539. rename( element, newName ) {
  540. if ( !( element instanceof Element ) ) {
  541. /**
  542. * Trying to rename an object which is not an instance of Element.
  543. *
  544. * @error batch-rename-not-element-instance
  545. */
  546. throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
  547. }
  548. const delta = new RenameDelta();
  549. this.addDelta( delta );
  550. const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.document.version );
  551. delta.addOperation( renameOperation );
  552. this.document.applyOperation( renameOperation );
  553. }
  554. /**
  555. * Splits an element at the given position.
  556. *
  557. * The element cannot be a root element, as root element cannot be split. The `batch-split-element-no-parent` error
  558. * will be thrown if you try to split an element with no parent.
  559. *
  560. * @param {module:engine/model/position~Position} position Position of split.
  561. */
  562. split( position ) {
  563. const delta = new SplitDelta();
  564. this.addDelta( delta );
  565. const splitElement = position.parent;
  566. if ( !splitElement.parent ) {
  567. /**
  568. * Element with no parent can not be split.
  569. *
  570. * @error batch-split-element-no-parent
  571. */
  572. throw new CKEditorError( 'batch-split-element-no-parent: Element with no parent can not be split.' );
  573. }
  574. const copy = new Element( splitElement.name, splitElement.getAttributes() );
  575. const insert = new InsertOperation(
  576. Position.createAfter( splitElement ),
  577. copy,
  578. this.document.version
  579. );
  580. delta.addOperation( insert );
  581. this.document.applyOperation( insert );
  582. const move = new MoveOperation(
  583. position,
  584. splitElement.maxOffset - position.offset,
  585. Position.createFromParentAndOffset( copy, 0 ),
  586. this.document.version
  587. );
  588. move.isSticky = true;
  589. delta.addOperation( move );
  590. this.document.applyOperation( move );
  591. }
  592. /**
  593. * Wraps given range with given element or with a new element with specified name, if string has been passed.
  594. * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, error will be thrown.
  595. *
  596. * @param {module:engine/model/range~Range} range Range to wrap.
  597. * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
  598. */
  599. wrap( range, elementOrString ) {
  600. if ( !range.isFlat ) {
  601. /**
  602. * Range to wrap is not flat.
  603. *
  604. * @error batch-wrap-range-not-flat
  605. */
  606. throw new CKEditorError( 'batch-wrap-range-not-flat: Range to wrap is not flat.' );
  607. }
  608. const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
  609. if ( element.childCount > 0 ) {
  610. /**
  611. * Element to wrap with is not empty.
  612. *
  613. * @error batch-wrap-element-not-empty
  614. */
  615. throw new CKEditorError( 'batch-wrap-element-not-empty: Element to wrap with is not empty.' );
  616. }
  617. if ( element.parent !== null ) {
  618. /**
  619. * Element to wrap with is already attached to a tree model.
  620. *
  621. * @error batch-wrap-element-attached
  622. */
  623. throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' );
  624. }
  625. const delta = new WrapDelta();
  626. this.addDelta( delta );
  627. const insert = new InsertOperation( range.end, element, this.document.version );
  628. delta.addOperation( insert );
  629. this.document.applyOperation( insert );
  630. const targetPosition = Position.createFromParentAndOffset( element, 0 );
  631. const move = new MoveOperation(
  632. range.start,
  633. range.end.offset - range.start.offset,
  634. targetPosition,
  635. this.document.version
  636. );
  637. delta.addOperation( move );
  638. this.document.applyOperation( move );
  639. }
  640. /**
  641. * Unwraps children of the given element – all its children are moved before it and then the element is removed.
  642. * Throws error if you try to unwrap an element which does not have a parent.
  643. *
  644. * @param {module:engine/model/element~Element} element Element to unwrap.
  645. */
  646. unwrap( element ) {
  647. if ( element.parent === null ) {
  648. /**
  649. * Trying to unwrap an element which has no parent.
  650. *
  651. * @error batch-unwrap-element-no-parent
  652. */
  653. throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
  654. }
  655. const delta = new UnwrapDelta();
  656. this.addDelta( delta );
  657. const sourcePosition = Position.createFromParentAndOffset( element, 0 );
  658. const move = new MoveOperation(
  659. sourcePosition,
  660. element.maxOffset,
  661. Position.createBefore( element ),
  662. this.document.version
  663. );
  664. move.isSticky = true;
  665. delta.addOperation( move );
  666. this.document.applyOperation( move );
  667. // Computing new position because we moved some nodes before `element`.
  668. // If we would cache `Position.createBefore( element )` we remove wrong node.
  669. const graveyard = this.document.graveyard;
  670. const gyPosition = new Position( graveyard, [ 0 ] );
  671. const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.document.version );
  672. delta.addOperation( remove );
  673. this.document.applyOperation( remove );
  674. }
  675. /**
  676. * Adds or updates {@link module:engine/model/markercollection~Marker marker} with given name to given `range`.
  677. *
  678. * If passed name is a name of already existing marker (or {@link module:engine/model/markercollection~Marker Marker} instance
  679. * is passed), `range` parameter may be omitted. In this case marker will not be updated in
  680. * {@link module:engine/model/document~Document#markers document marker collection}. However the marker will be added to
  681. * the document history. This may be important for other features, like undo. From document history point of view, it will
  682. * look like the marker was created and added to the document at the moment when it is set using this method.
  683. *
  684. * This is useful if the marker is created before it can be added to document history (e.g. a feature creating the marker
  685. * is waiting for additional data, etc.). In this case, the marker may be first created directly through
  686. * {@link module:engine/model/markercollection~MarkerCollection MarkerCollection API} and only later added using `Batch` API.
  687. *
  688. * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to add or update.
  689. * @param {module:engine/model/range~Range} [newRange] Marker range.
  690. */
  691. setMarker( markerOrName, newRange ) {
  692. const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
  693. const currentMarker = this.document.markers.get( name );
  694. if ( !newRange && !currentMarker ) {
  695. /**
  696. * Range parameter is required when adding a new marker.
  697. *
  698. * @error batch-setMarker-no-range
  699. */
  700. throw new CKEditorError( 'batch-setMarker-no-range: Range parameter is required when adding a new marker.' );
  701. }
  702. const currentRange = currentMarker ? currentMarker.getRange() : null;
  703. if ( !newRange ) {
  704. // If `newRange` is not given, treat this as synchronizing existing marker.
  705. // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
  706. addMarkerOperation( this, name, null, currentRange );
  707. } else {
  708. // Just change marker range.
  709. addMarkerOperation( this, name, currentRange, newRange );
  710. }
  711. }
  712. /**
  713. * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
  714. *
  715. * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
  716. */
  717. removeMarker( markerOrName ) {
  718. const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
  719. if ( !this.document.markers.has( name ) ) {
  720. /**
  721. * Trying to remove marker which does not exist.
  722. *
  723. * @error batch-removeMarker-no-marker
  724. */
  725. throw new CKEditorError( 'batch-removeMarker-no-marker: Trying to remove marker which does not exist.' );
  726. }
  727. const oldRange = this.document.markers.get( name ).getRange();
  728. addMarkerOperation( this, name, oldRange, null );
  729. }
  730. }
  731. // Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
  732. //
  733. // Because attribute operation needs to have the same attribute value on the whole range, this function splits
  734. // the range into smaller parts.
  735. //
  736. // @private
  737. // @param {module:engine/model/batch~Batch} batch
  738. // @param {String} key Attribute key.
  739. // @param {*} value Attribute new value.
  740. // @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
  741. function setAttributeToRange( batch, key, value, range ) {
  742. const delta = new AttributeDelta();
  743. const doc = batch.document;
  744. // Position of the last split, the beginning of the new range.
  745. let lastSplitPosition = range.start;
  746. // Currently position in the scanning range. Because we need value after the position, it is not a current
  747. // position of the iterator but the previous one (we need to iterate one more time to get the value after).
  748. let position;
  749. // Value before the currently position.
  750. let valueBefore;
  751. // Value after the currently position.
  752. let valueAfter;
  753. for ( const val of range ) {
  754. valueAfter = val.item.getAttribute( key );
  755. // At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
  756. // because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
  757. if ( position && valueBefore != valueAfter ) {
  758. // if valueBefore == value there is nothing to change, so we add operation only if these values are different.
  759. if ( valueBefore != value ) {
  760. addOperation();
  761. }
  762. lastSplitPosition = position;
  763. }
  764. position = val.nextPosition;
  765. valueBefore = valueAfter;
  766. }
  767. // Because position in the loop is not the iterator position (see let position comment), the last position in
  768. // the while loop will be last but one position in the range. We need to check the last position manually.
  769. if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
  770. addOperation();
  771. }
  772. function addOperation() {
  773. // Add delta to the batch only if there is at least operation in the delta. Add delta only once.
  774. if ( delta.operations.length === 0 ) {
  775. batch.addDelta( delta );
  776. }
  777. const range = new Range( lastSplitPosition, position );
  778. const operation = new AttributeOperation( range, key, valueBefore, value, doc.version );
  779. delta.addOperation( operation );
  780. doc.applyOperation( operation );
  781. }
  782. }
  783. // Sets given attribute to the given node. When attribute value is null then attribute will be removed.
  784. //
  785. // @private
  786. // @param {module:engine/model/batch~Batch} batch
  787. // @param {String} key Attribute key.
  788. // @param {*} value Attribute new value.
  789. // @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
  790. function setAttributeToItem( batch, key, value, item ) {
  791. const doc = batch.document;
  792. const previousValue = item.getAttribute( key );
  793. let range, operation;
  794. if ( previousValue != value ) {
  795. const delta = item.root === item ? new RootAttributeDelta() : new AttributeDelta();
  796. batch.addDelta( delta );
  797. if ( item.root === item ) {
  798. // If we change attributes of root element, we have to use `RootAttributeOperation`.
  799. operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
  800. } else {
  801. if ( item.is( 'element' ) ) {
  802. // If we change the attribute of the element, we do not want to change attributes of its children, so
  803. // the end of the range cannot be after the closing tag, it should be inside that element, before any of
  804. // it's children, so the range will contain only the opening tag.
  805. range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
  806. } else {
  807. // If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
  808. // all characters represented by it.
  809. range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
  810. }
  811. operation = new AttributeOperation( range, key, previousValue, value, doc.version );
  812. }
  813. delta.addOperation( operation );
  814. doc.applyOperation( operation );
  815. }
  816. }
  817. // Creates and adds marker operation to {@link module:engine/model/delta/delta~Delta delta}.
  818. //
  819. // @private
  820. // @param {module:engine/model/batch~Batch} batch
  821. // @param {String} name Marker name.
  822. // @param {module:engine/model/range~Range} oldRange Marker range before the change.
  823. // @param {module:engine/model/range~Range} newRange Marker range after the change.
  824. function addMarkerOperation( batch, name, oldRange, newRange ) {
  825. const doc = batch.document;
  826. const delta = new MarkerDelta();
  827. const operation = new MarkerOperation( name, oldRange, newRange, doc.markers, doc.version );
  828. batch.addDelta( delta );
  829. delta.addOperation( operation );
  830. doc.applyOperation( operation );
  831. }
  832. // Returns `true` if both root elements are the same element or both are documents root elements.
  833. //
  834. // Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
  835. // within the same document fragment), but when element supposed to be moved from document fragment to the document, or
  836. // to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
  837. // collaboration may track changes on the document but ignore changes on detached fragments and should not get
  838. // unexpected `move` operation.
  839. function isSameTree( rootA, rootB ) {
  840. if ( rootA === rootB ) {
  841. return true;
  842. }
  843. return rootA instanceof RootElement && rootB instanceof RootElement;
  844. }