writer.js 43 KB

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