writer.js 36 KB

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