writer.js 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578
  1. /**
  2. * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module engine/model/writer
  7. */
  8. import AttributeOperation from './operation/attributeoperation';
  9. import DetachOperation from './operation/detachoperation';
  10. import InsertOperation from './operation/insertoperation';
  11. import MarkerOperation from './operation/markeroperation';
  12. import MoveOperation from './operation/moveoperation';
  13. import RenameOperation from './operation/renameoperation';
  14. import RootAttributeOperation from './operation/rootattributeoperation';
  15. import SplitOperation from './operation/splitoperation';
  16. import MergeOperation from './operation/mergeoperation';
  17. import DocumentFragment from './documentfragment';
  18. import Text from './text';
  19. import Element from './element';
  20. import RootElement from './rootelement';
  21. import Position from './position';
  22. import Range from './range.js';
  23. import DocumentSelection from './documentselection';
  24. import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
  25. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  26. /**
  27. * The model can only be modified by using the writer. It should be used whenever you want to create a node, modify
  28. * child nodes, attributes or text, set the selection's position and its attributes.
  29. *
  30. * The instance of the writer is only available in the {@link module:engine/model/model~Model#change `change()`} or
  31. * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`}.
  32. *
  33. * model.change( writer => {
  34. * writer.insertText( 'foo', paragraph, 'end' );
  35. * } );
  36. *
  37. * Note that the writer should never be stored and used outside of the `change()` and
  38. * `enqueueChange()` blocks.
  39. *
  40. * Note that writer's methods do not check the {@link module:engine/model/schema~Schema}. It is possible
  41. * to create incorrect model structures by using the writer. Read more about in
  42. * {@glink framework/guides/deep-dive/schema#who-checks-the-schema "Who checks the schema?"}.
  43. *
  44. * @see module:engine/model/model~Model#change
  45. * @see module:engine/model/model~Model#enqueueChange
  46. */
  47. export default class Writer {
  48. /**
  49. * Creates a writer instance.
  50. *
  51. * **Note:** It is not recommended to use it directly. Use {@link module:engine/model/model~Model#change `Model#change()`} or
  52. * {@link module:engine/model/model~Model#enqueueChange `Model#enqueueChange()`} instead.
  53. *
  54. * @protected
  55. * @param {module:engine/model/model~Model} model
  56. * @param {module:engine/model/batch~Batch} batch
  57. */
  58. constructor( model, batch ) {
  59. /**
  60. * Instance of the model on which this writer operates.
  61. *
  62. * @readonly
  63. * @type {module:engine/model/model~Model}
  64. */
  65. this.model = model;
  66. /**
  67. * The batch to which this writer will add changes.
  68. *
  69. * @readonly
  70. * @type {module:engine/model/batch~Batch}
  71. */
  72. this.batch = batch;
  73. }
  74. /**
  75. * Creates a new {@link module:engine/model/text~Text text node}.
  76. *
  77. * writer.createText( 'foo' );
  78. * writer.createText( 'foo', { bold: true } );
  79. *
  80. * @param {String} data Text data.
  81. * @param {Object} [attributes] Text attributes.
  82. * @returns {module:engine/model/text~Text} Created text node.
  83. */
  84. createText( data, attributes ) {
  85. return new Text( data, attributes );
  86. }
  87. /**
  88. * Creates a new {@link module:engine/model/element~Element element}.
  89. *
  90. * writer.createElement( 'paragraph' );
  91. * writer.createElement( 'paragraph', { alignment: 'center' } );
  92. *
  93. * @param {String} name Name of the element.
  94. * @param {Object} [attributes] Elements attributes.
  95. * @returns {module:engine/model/element~Element} Created element.
  96. */
  97. createElement( name, attributes ) {
  98. return new Element( name, attributes );
  99. }
  100. /**
  101. * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
  102. *
  103. * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
  104. */
  105. createDocumentFragment() {
  106. return new DocumentFragment();
  107. }
  108. /**
  109. * Creates a copy of the element and returns it. Created element has the same name and attributes as the original element.
  110. * If clone is deep, the original element's children are also cloned. If not, then empty element is returned.
  111. *
  112. * @param {module:engine/model/element~Element} element The element to clone.
  113. * @param {Boolean} [deep=true] If set to `true` clones element and all its children recursively. When set to `false`,
  114. * element will be cloned without any child.
  115. */
  116. cloneElement( element, deep = true ) {
  117. return element._clone( deep );
  118. }
  119. /**
  120. * Inserts item on given position.
  121. *
  122. * const paragraph = writer.createElement( 'paragraph' );
  123. * writer.insert( paragraph, position );
  124. *
  125. * Instead of using position you can use parent and offset:
  126. *
  127. * const text = writer.createText( 'foo' );
  128. * writer.insert( text, paragraph, 5 );
  129. *
  130. * You can also use `end` instead of the offset to insert at the end:
  131. *
  132. * const text = writer.createText( 'foo' );
  133. * writer.insert( text, paragraph, 'end' );
  134. *
  135. * Or insert before or after another element:
  136. *
  137. * const paragraph = writer.createElement( 'paragraph' );
  138. * writer.insert( paragraph, anotherParagraph, 'after' );
  139. *
  140. * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
  141. *
  142. * Note that if the item already has parent it will be removed from the previous parent.
  143. *
  144. * Note that you cannot re-insert a node from a document to a different document or a document fragment. In this case,
  145. * `model-writer-insert-forbidden-move` is thrown.
  146. *
  147. * If you want to move {@link module:engine/model/range~Range range} instead of an
  148. * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
  149. *
  150. * **Note:** For a paste-like content insertion mechanism see
  151. * {@link module:engine/model/model~Model#insertContent `model.insertContent()`}.
  152. *
  153. * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} item Item or document
  154. * fragment to insert.
  155. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  156. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  157. * second parameter is a {@link module:engine/model/item~Item model item}.
  158. */
  159. insert( item, itemOrPosition, offset = 0 ) {
  160. this._assertWriterUsedCorrectly();
  161. if ( item instanceof Text && item.data == '' ) {
  162. return;
  163. }
  164. const position = Position._createAt( itemOrPosition, offset );
  165. // If item has a parent already.
  166. if ( item.parent ) {
  167. // We need to check if item is going to be inserted within the same document.
  168. if ( isSameTree( item.root, position.root ) ) {
  169. // If it's we just need to move it.
  170. this.move( Range._createOn( item ), position );
  171. return;
  172. }
  173. // If it isn't the same root.
  174. else {
  175. if ( item.root.document ) {
  176. /**
  177. * Cannot move a node from a document to a different tree.
  178. * It is forbidden to move a node that was already in a document outside of it.
  179. *
  180. * @error model-writer-insert-forbidden-move
  181. */
  182. throw new CKEditorError(
  183. 'model-writer-insert-forbidden-move: ' +
  184. 'Cannot move a node from a document to a different tree. ' +
  185. 'It is forbidden to move a node that was already in a document outside of it.',
  186. this
  187. );
  188. } else {
  189. // Move between two different document fragments or from document fragment to a document is possible.
  190. // In that case, remove the item from it's original parent.
  191. this.remove( item );
  192. }
  193. }
  194. }
  195. const version = position.root.document ? position.root.document.version : null;
  196. const insert = new InsertOperation( position, item, version );
  197. if ( item instanceof Text ) {
  198. insert.shouldReceiveAttributes = true;
  199. }
  200. this.batch.addOperation( insert );
  201. this.model.applyOperation( insert );
  202. // When element is a DocumentFragment we need to move its markers to Document#markers.
  203. if ( item instanceof DocumentFragment ) {
  204. for ( const [ markerName, markerRange ] of item.markers ) {
  205. // We need to migrate marker range from DocumentFragment to Document.
  206. const rangeRootPosition = Position._createAt( markerRange.root, 0 );
  207. const range = new Range(
  208. markerRange.start._getCombined( rangeRootPosition, position ),
  209. markerRange.end._getCombined( rangeRootPosition, position )
  210. );
  211. const options = { range, usingOperation: true, affectsData: true };
  212. if ( this.model.markers.has( markerName ) ) {
  213. this.updateMarker( markerName, options );
  214. } else {
  215. this.addMarker( markerName, options );
  216. }
  217. }
  218. }
  219. }
  220. /**
  221. * Creates and inserts text on given position. You can optionally set text attributes:
  222. *
  223. * writer.insertText( 'foo', position );
  224. * writer.insertText( 'foo', { bold: true }, position );
  225. *
  226. * Instead of using position you can use parent and offset or define that text should be inserted at the end
  227. * or before or after other node:
  228. *
  229. * // Inserts 'foo' in paragraph, at offset 5:
  230. * writer.insertText( 'foo', paragraph, 5 );
  231. * // Inserts 'foo' at the end of a paragraph:
  232. * writer.insertText( 'foo', paragraph, 'end' );
  233. * // Inserts 'foo' after an image:
  234. * writer.insertText( 'foo', image, 'after' );
  235. *
  236. * These parameters work in the same way as {@link #createPositionAt `writer.createPositionAt()`}.
  237. *
  238. * @param {String} data Text data.
  239. * @param {Object} [attributes] Text attributes.
  240. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  241. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  242. * third parameter is a {@link module:engine/model/item~Item model item}.
  243. */
  244. insertText( text, attributes, itemOrPosition, offset ) {
  245. if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
  246. this.insert( this.createText( text ), attributes, itemOrPosition );
  247. } else {
  248. this.insert( this.createText( text, attributes ), itemOrPosition, offset );
  249. }
  250. }
  251. /**
  252. * Creates and inserts element on given position. You can optionally set attributes:
  253. *
  254. * writer.insertElement( 'paragraph', position );
  255. * writer.insertElement( 'paragraph', { alignment: 'center' }, position );
  256. *
  257. * Instead of using position you can use parent and offset or define that text should be inserted at the end
  258. * or before or after other node:
  259. *
  260. * // Inserts paragraph in the root at offset 5:
  261. * writer.insertElement( 'paragraph', root, 5 );
  262. * // Inserts paragraph at the end of a blockquote:
  263. * writer.insertElement( 'paragraph', blockquote, 'end' );
  264. * // Inserts after an image:
  265. * writer.insertElement( 'paragraph', image, 'after' );
  266. *
  267. * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
  268. *
  269. * @param {String} name Name of the element.
  270. * @param {Object} [attributes] Elements attributes.
  271. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  272. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  273. * third parameter is a {@link module:engine/model/item~Item model item}.
  274. */
  275. insertElement( name, attributes, itemOrPosition, offset ) {
  276. if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
  277. this.insert( this.createElement( name ), attributes, itemOrPosition );
  278. } else {
  279. this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
  280. }
  281. }
  282. /**
  283. * Inserts item at the end of the given parent.
  284. *
  285. * const paragraph = writer.createElement( 'paragraph' );
  286. * writer.append( paragraph, root );
  287. *
  288. * Note that if the item already has parent it will be removed from the previous parent.
  289. *
  290. * If you want to move {@link module:engine/model/range~Range range} instead of an
  291. * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
  292. *
  293. * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
  294. * item Item or document fragment to insert.
  295. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  296. */
  297. append( item, parent ) {
  298. this.insert( item, parent, 'end' );
  299. }
  300. /**
  301. * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
  302. *
  303. * writer.appendText( 'foo', paragraph );
  304. * writer.appendText( 'foo', { bold: true }, paragraph );
  305. *
  306. * @param {String} text Text data.
  307. * @param {Object} [attributes] Text attributes.
  308. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  309. */
  310. appendText( text, attributes, parent ) {
  311. if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
  312. this.insert( this.createText( text ), attributes, 'end' );
  313. } else {
  314. this.insert( this.createText( text, attributes ), parent, 'end' );
  315. }
  316. }
  317. /**
  318. * Creates element and inserts it at the end of the parent. You can optionally set attributes:
  319. *
  320. * writer.appendElement( 'paragraph', root );
  321. * writer.appendElement( 'paragraph', { alignment: 'center' }, root );
  322. *
  323. * @param {String} name Name of the element.
  324. * @param {Object} [attributes] Elements attributes.
  325. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
  326. */
  327. appendElement( name, attributes, parent ) {
  328. if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
  329. this.insert( this.createElement( name ), attributes, 'end' );
  330. } else {
  331. this.insert( this.createElement( name, attributes ), parent, 'end' );
  332. }
  333. }
  334. /**
  335. * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
  336. * or on a {@link module:engine/model/range~Range range}.
  337. *
  338. * @param {String} key Attribute key.
  339. * @param {*} value Attribute new value.
  340. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  341. * Model item or range on which the attribute will be set.
  342. */
  343. setAttribute( key, value, itemOrRange ) {
  344. this._assertWriterUsedCorrectly();
  345. if ( itemOrRange instanceof Range ) {
  346. const ranges = itemOrRange.getMinimalFlatRanges();
  347. for ( const range of ranges ) {
  348. setAttributeOnRange( this, key, value, range );
  349. }
  350. } else {
  351. setAttributeOnItem( this, key, value, itemOrRange );
  352. }
  353. }
  354. /**
  355. * Sets values of attributes on a {@link module:engine/model/item~Item model item}
  356. * or on a {@link module:engine/model/range~Range range}.
  357. *
  358. * writer.setAttributes( {
  359. * bold: true,
  360. * italic: true
  361. * }, range );
  362. *
  363. * @param {Object} attributes Attributes keys and values.
  364. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  365. * Model item or range on which the attributes will be set.
  366. */
  367. setAttributes( attributes, itemOrRange ) {
  368. for ( const [ key, val ] of toMap( attributes ) ) {
  369. this.setAttribute( key, val, itemOrRange );
  370. }
  371. }
  372. /**
  373. * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
  374. * or from a {@link module:engine/model/range~Range range}.
  375. *
  376. * @param {String} key Attribute key.
  377. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  378. * Model item or range from which the attribute will be removed.
  379. */
  380. removeAttribute( key, itemOrRange ) {
  381. this._assertWriterUsedCorrectly();
  382. if ( itemOrRange instanceof Range ) {
  383. const ranges = itemOrRange.getMinimalFlatRanges();
  384. for ( const range of ranges ) {
  385. setAttributeOnRange( this, key, null, range );
  386. }
  387. } else {
  388. setAttributeOnItem( this, key, null, itemOrRange );
  389. }
  390. }
  391. /**
  392. * Removes all attributes from all elements in the range or from the given item.
  393. *
  394. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
  395. * Model item or range from which all attributes will be removed.
  396. */
  397. clearAttributes( itemOrRange ) {
  398. this._assertWriterUsedCorrectly();
  399. const removeAttributesFromItem = item => {
  400. for ( const attribute of item.getAttributeKeys() ) {
  401. this.removeAttribute( attribute, item );
  402. }
  403. };
  404. if ( !( itemOrRange instanceof Range ) ) {
  405. removeAttributesFromItem( itemOrRange );
  406. } else {
  407. for ( const item of itemOrRange.getItems() ) {
  408. removeAttributesFromItem( item );
  409. }
  410. }
  411. }
  412. /**
  413. * Moves all items in the source range to the target position.
  414. *
  415. * writer.move( sourceRange, targetPosition );
  416. *
  417. * Instead of the target position you can use parent and offset or define that range should be moved to the end
  418. * or before or after chosen item:
  419. *
  420. * // Moves all items in the range to the paragraph at offset 5:
  421. * writer.move( sourceRange, paragraph, 5 );
  422. * // Moves all items in the range to the end of a blockquote:
  423. * writer.move( sourceRange, blockquote, 'end' );
  424. * // Moves all items in the range to a position after an image:
  425. * writer.move( sourceRange, image, 'after' );
  426. *
  427. * These parameters works the same way as {@link #createPositionAt `writer.createPositionAt()`}.
  428. *
  429. * Note that items can be moved only within the same tree. It means that you can move items within the same root
  430. * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
  431. * but you can not move items from document fragment to the document or from one detached element to another. Use
  432. * {@link module:engine/model/writer~Writer#insert} in such cases.
  433. *
  434. * @param {module:engine/model/range~Range} range Source range.
  435. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  436. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  437. * second parameter is a {@link module:engine/model/item~Item model item}.
  438. */
  439. move( range, itemOrPosition, offset ) {
  440. this._assertWriterUsedCorrectly();
  441. if ( !( range instanceof Range ) ) {
  442. /**
  443. * Invalid range to move.
  444. *
  445. * @error writer-move-invalid-range
  446. */
  447. throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.', this );
  448. }
  449. if ( !range.isFlat ) {
  450. /**
  451. * Range to move is not flat.
  452. *
  453. * @error writer-move-range-not-flat
  454. */
  455. throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.', this );
  456. }
  457. const position = Position._createAt( itemOrPosition, offset );
  458. // Do not move anything if the move target is same as moved range start.
  459. if ( position.isEqual( range.start ) ) {
  460. return;
  461. }
  462. // If part of the marker is removed, create additional marker operation for undo purposes.
  463. this._addOperationForAffectedMarkers( 'move', range );
  464. if ( !isSameTree( range.root, position.root ) ) {
  465. /**
  466. * Range is going to be moved within not the same document. Please use
  467. * {@link module:engine/model/writer~Writer#insert insert} instead.
  468. *
  469. * @error writer-move-different-document
  470. */
  471. throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.', this );
  472. }
  473. const version = range.root.document ? range.root.document.version : null;
  474. const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, version );
  475. this.batch.addOperation( operation );
  476. this.model.applyOperation( operation );
  477. }
  478. /**
  479. * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
  480. *
  481. * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
  482. */
  483. remove( itemOrRange ) {
  484. this._assertWriterUsedCorrectly();
  485. const rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn( itemOrRange );
  486. const ranges = rangeToRemove.getMinimalFlatRanges().reverse();
  487. for ( const flat of ranges ) {
  488. // If part of the marker is removed, create additional marker operation for undo purposes.
  489. this._addOperationForAffectedMarkers( 'move', flat );
  490. applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model );
  491. }
  492. }
  493. /**
  494. * Merges two siblings at the given position.
  495. *
  496. * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
  497. * `writer-merge-no-element-after` error will be thrown.
  498. *
  499. * @param {module:engine/model/position~Position} position Position between merged elements.
  500. */
  501. merge( position ) {
  502. this._assertWriterUsedCorrectly();
  503. const nodeBefore = position.nodeBefore;
  504. const nodeAfter = position.nodeAfter;
  505. // If part of the marker is removed, create additional marker operation for undo purposes.
  506. this._addOperationForAffectedMarkers( 'merge', position );
  507. if ( !( nodeBefore instanceof Element ) ) {
  508. /**
  509. * Node before merge position must be an element.
  510. *
  511. * @error writer-merge-no-element-before
  512. */
  513. throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.', this );
  514. }
  515. if ( !( nodeAfter instanceof Element ) ) {
  516. /**
  517. * Node after merge position must be an element.
  518. *
  519. * @error writer-merge-no-element-after
  520. */
  521. throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.', this );
  522. }
  523. if ( !position.root.document ) {
  524. this._mergeDetached( position );
  525. } else {
  526. this._merge( position );
  527. }
  528. }
  529. /**
  530. * Shortcut for {@link module:engine/model/model~Model#createPositionFromPath `Model#createPositionFromPath()`}.
  531. *
  532. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
  533. * @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
  534. * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
  535. * See {@link module:engine/model/position~PositionStickiness}.
  536. * @returns {module:engine/model/position~Position}
  537. */
  538. createPositionFromPath( root, path, stickiness ) {
  539. return this.model.createPositionFromPath( root, path, stickiness );
  540. }
  541. /**
  542. * Shortcut for {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}.
  543. *
  544. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  545. * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
  546. * first parameter is a {@link module:engine/model/item~Item model item}.
  547. * @returns {module:engine/model/position~Position}
  548. */
  549. createPositionAt( itemOrPosition, offset ) {
  550. return this.model.createPositionAt( itemOrPosition, offset );
  551. }
  552. /**
  553. * Shortcut for {@link module:engine/model/model~Model#createPositionAfter `Model#createPositionAfter()`}.
  554. *
  555. * @param {module:engine/model/item~Item} item Item after which the position should be placed.
  556. * @returns {module:engine/model/position~Position}
  557. */
  558. createPositionAfter( item ) {
  559. return this.model.createPositionAfter( item );
  560. }
  561. /**
  562. * Shortcut for {@link module:engine/model/model~Model#createPositionBefore `Model#createPositionBefore()`}.
  563. *
  564. * @param {module:engine/model/item~Item} item Item after which the position should be placed.
  565. * @returns {module:engine/model/position~Position}
  566. */
  567. createPositionBefore( item ) {
  568. return this.model.createPositionBefore( item );
  569. }
  570. /**
  571. * Shortcut for {@link module:engine/model/model~Model#createRange `Model#createRange()`}.
  572. *
  573. * @param {module:engine/model/position~Position} start Start position.
  574. * @param {module:engine/model/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
  575. * @returns {module:engine/model/range~Range}
  576. */
  577. createRange( start, end ) {
  578. return this.model.createRange( start, end );
  579. }
  580. /**
  581. * Shortcut for {@link module:engine/model/model~Model#createRangeIn `Model#createRangeIn()`}.
  582. *
  583. * @param {module:engine/model/element~Element} element Element which is a parent for the range.
  584. * @returns {module:engine/model/range~Range}
  585. */
  586. createRangeIn( element ) {
  587. return this.model.createRangeIn( element );
  588. }
  589. /**
  590. * Shortcut for {@link module:engine/model/model~Model#createRangeOn `Model#createRangeOn()`}.
  591. *
  592. * @param {module:engine/model/element~Element} element Element which is a parent for the range.
  593. * @returns {module:engine/model/range~Range}
  594. */
  595. createRangeOn( element ) {
  596. return this.model.createRangeOn( element );
  597. }
  598. /**
  599. * Shortcut for {@link module:engine/model/model~Model#createSelection `Model#createSelection()`}.
  600. *
  601. * @param {module:engine/model/selection~Selectable} selectable
  602. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  603. * @param {Object} [options]
  604. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  605. * @returns {module:engine/model/selection~Selection}
  606. */
  607. createSelection( selectable, placeOrOffset, options ) {
  608. return this.model.createSelection( selectable, placeOrOffset, options );
  609. }
  610. /**
  611. * Performs merge action in a detached tree.
  612. *
  613. * @private
  614. * @param {module:engine/model/position~Position} position Position between merged elements.
  615. */
  616. _mergeDetached( position ) {
  617. const nodeBefore = position.nodeBefore;
  618. const nodeAfter = position.nodeAfter;
  619. this.move( Range._createIn( nodeAfter ), Position._createAt( nodeBefore, 'end' ) );
  620. this.remove( nodeAfter );
  621. }
  622. /**
  623. * Performs merge action in a non-detached tree.
  624. *
  625. * @private
  626. * @param {module:engine/model/position~Position} position Position between merged elements.
  627. */
  628. _merge( position ) {
  629. const targetPosition = Position._createAt( position.nodeBefore, 'end' );
  630. const sourcePosition = Position._createAt( position.nodeAfter, 0 );
  631. const graveyard = position.root.document.graveyard;
  632. const graveyardPosition = new Position( graveyard, [ 0 ] );
  633. const version = position.root.document.version;
  634. const merge = new MergeOperation( sourcePosition, position.nodeAfter.maxOffset, targetPosition, graveyardPosition, version );
  635. this.batch.addOperation( merge );
  636. this.model.applyOperation( merge );
  637. }
  638. /**
  639. * Renames the given element.
  640. *
  641. * @param {module:engine/model/element~Element} element The element to rename.
  642. * @param {String} newName New element name.
  643. */
  644. rename( element, newName ) {
  645. this._assertWriterUsedCorrectly();
  646. if ( !( element instanceof Element ) ) {
  647. /**
  648. * Trying to rename an object which is not an instance of Element.
  649. *
  650. * @error writer-rename-not-element-instance
  651. */
  652. throw new CKEditorError(
  653. 'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.',
  654. this
  655. );
  656. }
  657. const version = element.root.document ? element.root.document.version : null;
  658. const renameOperation = new RenameOperation( Position._createBefore( element ), element.name, newName, version );
  659. this.batch.addOperation( renameOperation );
  660. this.model.applyOperation( renameOperation );
  661. }
  662. /**
  663. * Splits elements starting from the given position and going to the top of the model tree as long as given
  664. * `limitElement` is reached. When `limitElement` is not defined then only the parent of the given position will be split.
  665. *
  666. * The element needs to have a parent. It cannot be a root element nor a document fragment.
  667. * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
  668. *
  669. * @param {module:engine/model/position~Position} position Position of split.
  670. * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
  671. * @returns {Object} result Split result.
  672. * @returns {module:engine/model/position~Position} result.position Position between split elements.
  673. * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ends
  674. * at the beginning of the first copy element.
  675. */
  676. split( position, limitElement ) {
  677. this._assertWriterUsedCorrectly();
  678. let splitElement = position.parent;
  679. if ( !splitElement.parent ) {
  680. /**
  681. * Element with no parent can not be split.
  682. *
  683. * @error writer-split-element-no-parent
  684. */
  685. throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.', this );
  686. }
  687. // When limit element is not defined lets set splitElement parent as limit.
  688. if ( !limitElement ) {
  689. limitElement = splitElement.parent;
  690. }
  691. if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
  692. throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.', this );
  693. }
  694. // We need to cache elements that will be created as a result of the first split because
  695. // we need to create a range from the end of the first split element to the beginning of the
  696. // first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
  697. let firstSplitElement, firstCopyElement;
  698. do {
  699. const version = splitElement.root.document ? splitElement.root.document.version : null;
  700. const howMany = splitElement.maxOffset - position.offset;
  701. const split = new SplitOperation( position, howMany, null, version );
  702. this.batch.addOperation( split );
  703. this.model.applyOperation( split );
  704. // Cache result of the first split.
  705. if ( !firstSplitElement && !firstCopyElement ) {
  706. firstSplitElement = splitElement;
  707. firstCopyElement = position.parent.nextSibling;
  708. }
  709. position = this.createPositionAfter( position.parent );
  710. splitElement = position.parent;
  711. } while ( splitElement !== limitElement );
  712. return {
  713. position,
  714. range: new Range( Position._createAt( firstSplitElement, 'end' ), Position._createAt( firstCopyElement, 0 ) )
  715. };
  716. }
  717. /**
  718. * Wraps the given range with the given element or with a new element (if a string was passed).
  719. *
  720. * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat `Range#isFlat`}).
  721. * If not, an error will be thrown.
  722. *
  723. * @param {module:engine/model/range~Range} range Range to wrap.
  724. * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
  725. */
  726. wrap( range, elementOrString ) {
  727. this._assertWriterUsedCorrectly();
  728. if ( !range.isFlat ) {
  729. /**
  730. * Range to wrap is not flat.
  731. *
  732. * @error writer-wrap-range-not-flat
  733. */
  734. throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.', this );
  735. }
  736. const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
  737. if ( element.childCount > 0 ) {
  738. /**
  739. * Element to wrap with is not empty.
  740. *
  741. * @error writer-wrap-element-not-empty
  742. */
  743. throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.', this );
  744. }
  745. if ( element.parent !== null ) {
  746. /**
  747. * Element to wrap with is already attached to a tree model.
  748. *
  749. * @error writer-wrap-element-attached
  750. */
  751. throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.', this );
  752. }
  753. this.insert( element, range.start );
  754. // Shift the range-to-wrap because we just inserted an element before that range.
  755. const shiftedRange = new Range( range.start.getShiftedBy( 1 ), range.end.getShiftedBy( 1 ) );
  756. this.move( shiftedRange, Position._createAt( element, 0 ) );
  757. }
  758. /**
  759. * Unwraps children of the given element – all its children are moved before it and then the element is removed.
  760. * Throws error if you try to unwrap an element which does not have a parent.
  761. *
  762. * @param {module:engine/model/element~Element} element Element to unwrap.
  763. */
  764. unwrap( element ) {
  765. this._assertWriterUsedCorrectly();
  766. if ( element.parent === null ) {
  767. /**
  768. * Trying to unwrap an element which has no parent.
  769. *
  770. * @error writer-unwrap-element-no-parent
  771. */
  772. throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.', this );
  773. }
  774. this.move( Range._createIn( element ), this.createPositionAfter( element ) );
  775. this.remove( element );
  776. }
  777. /**
  778. * Adds a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
  779. * changes in the document and updates its range automatically, when model tree changes.
  780. *
  781. * As the first parameter you can set marker name.
  782. *
  783. * The required `options.usingOperation` parameter lets you decide if the marker should be managed by operations or not. See
  784. * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
  785. * markers managed by operations and not-managed by operations.
  786. *
  787. * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
  788. * `true` when the marker change changes the data returned by the
  789. * {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
  790. * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
  791. * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
  792. *
  793. * Create marker directly base on marker's name:
  794. *
  795. * addMarker( markerName, { range, usingOperation: false } );
  796. *
  797. * Create marker using operation:
  798. *
  799. * addMarker( markerName, { range, usingOperation: true } );
  800. *
  801. * Create marker that affects the editor data:
  802. *
  803. * addMarker( markerName, { range, usingOperation: false, affectsData: true } );
  804. *
  805. * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
  806. *
  807. * @see module:engine/model/markercollection~Marker
  808. * @param {String} name Name of a marker to create - must be unique.
  809. * @param {Object} options
  810. * @param {Boolean} options.usingOperation Flag indicating that the marker should be added by MarkerOperation.
  811. * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
  812. * @param {module:engine/model/range~Range} options.range Marker range.
  813. * @param {Boolean} [options.affectsData=false] Flag indicating that the marker changes the editor data.
  814. * @returns {module:engine/model/markercollection~Marker} Marker that was set.
  815. */
  816. addMarker( name, options ) {
  817. this._assertWriterUsedCorrectly();
  818. if ( !options || typeof options.usingOperation != 'boolean' ) {
  819. /**
  820. * The `options.usingOperation` parameter is required when adding a new marker.
  821. *
  822. * @error writer-addMarker-no-usingOperation
  823. */
  824. throw new CKEditorError(
  825. 'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.',
  826. this
  827. );
  828. }
  829. const usingOperation = options.usingOperation;
  830. const range = options.range;
  831. const affectsData = options.affectsData === undefined ? false : options.affectsData;
  832. if ( this.model.markers.has( name ) ) {
  833. /**
  834. * Marker with provided name already exists.
  835. *
  836. * @error writer-addMarker-marker-exists
  837. */
  838. throw new CKEditorError( 'writer-addMarker-marker-exists: Marker with provided name already exists.', this );
  839. }
  840. if ( !range ) {
  841. /**
  842. * Range parameter is required when adding a new marker.
  843. *
  844. * @error writer-addMarker-no-range
  845. */
  846. throw new CKEditorError(
  847. 'writer-addMarker-no-range: Range parameter is required when adding a new marker.',
  848. this
  849. );
  850. }
  851. if ( !usingOperation ) {
  852. return this.model.markers._set( name, range, usingOperation, affectsData );
  853. }
  854. applyMarkerOperation( this, name, null, range, affectsData );
  855. return this.model.markers.get( name );
  856. }
  857. /**
  858. * Adds, updates or refreshes a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
  859. * changes in the document and updates its range automatically, when model tree changes. Still, it is possible to change the
  860. * marker's range directly using this method.
  861. *
  862. * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
  863. * name is created and returned.
  864. *
  865. * As the second parameter you can set the new marker data or leave this parameter as empty which will just refresh
  866. * the marker by triggering downcast conversion for it. Refreshing the marker is useful when you want to change
  867. * the marker {@link module:engine/view/element~Element view element} without changing any marker data.
  868. *
  869. * let isCommentActive = false;
  870. *
  871. * model.conversion.markerToHighlight( {
  872. * model: 'comment',
  873. * view: data => {
  874. * const classes = [ 'comment-marker' ];
  875. *
  876. * if ( isCommentActive ) {
  877. * classes.push( 'comment-marker--active' );
  878. * }
  879. *
  880. * return { classes };
  881. * }
  882. * } );
  883. *
  884. * // Change the property that indicates if marker is displayed as active or not.
  885. * isCommentActive = true;
  886. *
  887. * // And refresh the marker to convert it with additional class.
  888. * model.change( writer => writer.updateMarker( 'comment' ) );
  889. *
  890. * The `options.usingOperation` parameter lets you change if the marker should be managed by operations or not. See
  891. * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
  892. * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
  893. *
  894. * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
  895. * `true` when the marker change changes the data returned by
  896. * the {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
  897. * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
  898. * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
  899. *
  900. * Update marker directly base on marker's name:
  901. *
  902. * updateMarker( markerName, { range } );
  903. *
  904. * Update marker using operation:
  905. *
  906. * updateMarker( marker, { range, usingOperation: true } );
  907. * updateMarker( markerName, { range, usingOperation: true } );
  908. *
  909. * Change marker's option (start using operations to manage it):
  910. *
  911. * updateMarker( marker, { usingOperation: true } );
  912. *
  913. * Change marker's option (inform the engine, that the marker does not affect the data anymore):
  914. *
  915. * updateMarker( markerName, { affectsData: false } );
  916. *
  917. * @see module:engine/model/markercollection~Marker
  918. * @param {String|module:engine/model/markercollection~Marker} markerOrName Name of a marker to update, or a marker instance.
  919. * @param {Object} [options] If options object is not defined then marker will be refreshed by triggering
  920. * downcast conversion for this marker with the same data.
  921. * @param {module:engine/model/range~Range} [options.range] Marker range to update.
  922. * @param {Boolean} [options.usingOperation] Flag indicated whether the marker should be added by MarkerOperation.
  923. * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
  924. * @param {Boolean} [options.affectsData] Flag indicating that the marker changes the editor data.
  925. */
  926. updateMarker( markerOrName, options ) {
  927. this._assertWriterUsedCorrectly();
  928. const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
  929. const currentMarker = this.model.markers.get( markerName );
  930. if ( !currentMarker ) {
  931. /**
  932. * Marker with provided name does not exists.
  933. *
  934. * @error writer-updateMarker-marker-not-exists
  935. */
  936. throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.', this );
  937. }
  938. if ( !options ) {
  939. this.model.markers._refresh( currentMarker );
  940. return;
  941. }
  942. const hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
  943. const affectsDataDefined = typeof options.affectsData == 'boolean';
  944. // Use previously defined marker's affectsData if the property is not provided.
  945. const affectsData = affectsDataDefined ? options.affectsData : currentMarker.affectsData;
  946. if ( !hasUsingOperationDefined && !options.range && !affectsDataDefined ) {
  947. /**
  948. * One of the options is required - provide range, usingOperations or affectsData.
  949. *
  950. * @error writer-updateMarker-wrong-options
  951. */
  952. throw new CKEditorError(
  953. 'writer-updateMarker-wrong-options: One of the options is required - provide range, usingOperations or affectsData.',
  954. this
  955. );
  956. }
  957. const currentRange = currentMarker.getRange();
  958. const updatedRange = options.range ? options.range : currentRange;
  959. if ( hasUsingOperationDefined && options.usingOperation !== currentMarker.managedUsingOperations ) {
  960. // The marker type is changed so it's necessary to create proper operations.
  961. if ( options.usingOperation ) {
  962. // If marker changes to a managed one treat this as synchronizing existing marker.
  963. // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
  964. applyMarkerOperation( this, markerName, null, updatedRange, affectsData );
  965. } else {
  966. // If marker changes to a marker that do not use operations then we need to create additional operation
  967. // that removes that marker first.
  968. applyMarkerOperation( this, markerName, currentRange, null, affectsData );
  969. // Although not managed the marker itself should stay in model and its range should be preserver or changed to passed range.
  970. this.model.markers._set( markerName, updatedRange, undefined, affectsData );
  971. }
  972. return;
  973. }
  974. // Marker's type doesn't change so update it accordingly.
  975. if ( currentMarker.managedUsingOperations ) {
  976. applyMarkerOperation( this, markerName, currentRange, updatedRange, affectsData );
  977. } else {
  978. this.model.markers._set( markerName, updatedRange, undefined, affectsData );
  979. }
  980. }
  981. /**
  982. * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
  983. * The marker is removed accordingly to how it has been created, so if the marker was created using operation,
  984. * it will be destroyed using operation.
  985. *
  986. * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
  987. */
  988. removeMarker( markerOrName ) {
  989. this._assertWriterUsedCorrectly();
  990. const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
  991. if ( !this.model.markers.has( name ) ) {
  992. /**
  993. * Trying to remove marker which does not exist.
  994. *
  995. * @error writer-removeMarker-no-marker
  996. */
  997. throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.', this );
  998. }
  999. const marker = this.model.markers.get( name );
  1000. if ( !marker.managedUsingOperations ) {
  1001. this.model.markers._remove( name );
  1002. return;
  1003. }
  1004. const oldRange = marker.getRange();
  1005. applyMarkerOperation( this, name, oldRange, null, marker.affectsData );
  1006. }
  1007. /**
  1008. * Sets the document's selection (ranges and direction) to the specified location based on the given
  1009. * {@link module:engine/model/selection~Selectable selectable} or creates an empty selection if no arguments were passed.
  1010. *
  1011. * // Sets selection to the given range.
  1012. * const range = writer.createRange( start, end );
  1013. * writer.setSelection( range );
  1014. *
  1015. * // Sets selection to given ranges.
  1016. * const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
  1017. * writer.setSelection( ranges );
  1018. *
  1019. * // Sets selection to other selection.
  1020. * const otherSelection = writer.createSelection();
  1021. * writer.setSelection( otherSelection );
  1022. *
  1023. * // Sets selection to the given document selection.
  1024. * const documentSelection = model.document.selection;
  1025. * writer.setSelection( documentSelection );
  1026. *
  1027. * // Sets collapsed selection at the given position.
  1028. * const position = writer.createPosition( root, path );
  1029. * writer.setSelection( position );
  1030. *
  1031. * // Sets collapsed selection at the position of the given node and an offset.
  1032. * writer.setSelection( paragraph, offset );
  1033. *
  1034. * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
  1035. * that element and ends after the last child of that element.
  1036. *
  1037. * writer.setSelection( paragraph, 'in' );
  1038. *
  1039. * Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends just after the item.
  1040. *
  1041. * writer.setSelection( paragraph, 'on' );
  1042. *
  1043. * // Removes all selection's ranges.
  1044. * writer.setSelection( null );
  1045. *
  1046. * `Writer#setSelection()` allow passing additional options (`backward`) as the last argument.
  1047. *
  1048. * // Sets selection as backward.
  1049. * writer.setSelection( range, { backward: true } );
  1050. *
  1051. * Throws `writer-incorrect-use` error when the writer is used outside the `change()` block.
  1052. *
  1053. * @param {module:engine/model/selection~Selectable} selectable
  1054. * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
  1055. * @param {Object} [options]
  1056. * @param {Boolean} [options.backward] Sets this selection instance to be backward.
  1057. */
  1058. setSelection( selectable, placeOrOffset, options ) {
  1059. this._assertWriterUsedCorrectly();
  1060. this.model.document.selection._setTo( selectable, placeOrOffset, options );
  1061. }
  1062. /**
  1063. * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
  1064. *
  1065. * The location can be specified in the same form as
  1066. * {@link #createPositionAt `writer.createPositionAt()`} parameters.
  1067. *
  1068. * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
  1069. * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
  1070. * first parameter is a {@link module:engine/model/item~Item model item}.
  1071. */
  1072. setSelectionFocus( itemOrPosition, offset ) {
  1073. this._assertWriterUsedCorrectly();
  1074. this.model.document.selection._setFocus( itemOrPosition, offset );
  1075. }
  1076. /**
  1077. * Sets attribute(s) on the selection. If attribute with the same key already is set, it's value is overwritten.
  1078. *
  1079. * Using key and value pair:
  1080. *
  1081. * writer.setSelectionAttribute( 'italic', true );
  1082. *
  1083. * Using key-value object:
  1084. *
  1085. * writer.setSelectionAttribute( { italic: true, bold: false } );
  1086. *
  1087. * Using iterable object:
  1088. *
  1089. * writer.setSelectionAttribute( new Map( [ [ 'italic', true ] ] ) );
  1090. *
  1091. * @param {String|Object|Iterable.<*>} keyOrObjectOrIterable Key of the attribute to set
  1092. * or object / iterable of key => value attribute pairs.
  1093. * @param {*} [value] Attribute value.
  1094. */
  1095. setSelectionAttribute( keyOrObjectOrIterable, value ) {
  1096. this._assertWriterUsedCorrectly();
  1097. if ( typeof keyOrObjectOrIterable === 'string' ) {
  1098. this._setSelectionAttribute( keyOrObjectOrIterable, value );
  1099. } else {
  1100. for ( const [ key, value ] of toMap( keyOrObjectOrIterable ) ) {
  1101. this._setSelectionAttribute( key, value );
  1102. }
  1103. }
  1104. }
  1105. /**
  1106. * Removes attribute(s) with given key(s) from the selection.
  1107. *
  1108. * Remove one attribute:
  1109. *
  1110. * writer.removeSelectionAttribute( 'italic' );
  1111. *
  1112. * Remove multiple attributes:
  1113. *
  1114. * writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
  1115. *
  1116. * @param {String|Iterable.<String>} keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
  1117. */
  1118. removeSelectionAttribute( keyOrIterableOfKeys ) {
  1119. this._assertWriterUsedCorrectly();
  1120. if ( typeof keyOrIterableOfKeys === 'string' ) {
  1121. this._removeSelectionAttribute( keyOrIterableOfKeys );
  1122. } else {
  1123. for ( const key of keyOrIterableOfKeys ) {
  1124. this._removeSelectionAttribute( key );
  1125. }
  1126. }
  1127. }
  1128. /**
  1129. * Temporarily changes the {@link module:engine/model/documentselection~DocumentSelection#isGravityOverridden gravity}
  1130. * of the selection from left to right.
  1131. *
  1132. * The gravity defines from which direction the selection inherits its attributes. If it's the default left gravity,
  1133. * then the selection (after being moved by the user) inherits attributes from its left-hand side.
  1134. * This method allows to temporarily override this behavior by forcing the gravity to the right.
  1135. *
  1136. * For the following model fragment:
  1137. *
  1138. * <$text bold="true" linkHref="url">bar[]</$text><$text bold="true">biz</$text>
  1139. *
  1140. * * Default gravity: selection will have the `bold` and `linkHref` attributes.
  1141. * * Overridden gravity: selection will have `bold` attribute.
  1142. *
  1143. * **Note**: It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
  1144. * of the process.
  1145. *
  1146. * @returns {String} The unique id which allows restoring the gravity.
  1147. */
  1148. overrideSelectionGravity() {
  1149. return this.model.document.selection._overrideGravity();
  1150. }
  1151. /**
  1152. * Restores {@link ~Writer#overrideSelectionGravity} gravity to default.
  1153. *
  1154. * Restoring the gravity is only possible using the unique identifier returned by
  1155. * {@link ~Writer#overrideSelectionGravity}. Note that the gravity remains overridden as long as won't be restored
  1156. * the same number of times it was overridden.
  1157. *
  1158. * @param {String} uid The unique id returned by {@link ~Writer#overrideSelectionGravity}.
  1159. */
  1160. restoreSelectionGravity( uid ) {
  1161. this.model.document.selection._restoreGravity( uid );
  1162. }
  1163. /**
  1164. * @private
  1165. * @param {String} key Key of the attribute to remove.
  1166. * @param {*} value Attribute value.
  1167. */
  1168. _setSelectionAttribute( key, value ) {
  1169. const selection = this.model.document.selection;
  1170. // Store attribute in parent element if the selection is collapsed in an empty node.
  1171. if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
  1172. const storeKey = DocumentSelection._getStoreAttributeKey( key );
  1173. this.setAttribute( storeKey, value, selection.anchor.parent );
  1174. }
  1175. selection._setAttribute( key, value );
  1176. }
  1177. /**
  1178. * @private
  1179. * @param {String} key Key of the attribute to remove.
  1180. */
  1181. _removeSelectionAttribute( key ) {
  1182. const selection = this.model.document.selection;
  1183. // Remove stored attribute from parent element if the selection is collapsed in an empty node.
  1184. if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
  1185. const storeKey = DocumentSelection._getStoreAttributeKey( key );
  1186. this.removeAttribute( storeKey, selection.anchor.parent );
  1187. }
  1188. selection._removeAttribute( key );
  1189. }
  1190. /**
  1191. * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
  1192. *
  1193. * @private
  1194. */
  1195. _assertWriterUsedCorrectly() {
  1196. /**
  1197. * Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()`} or
  1198. * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks.
  1199. *
  1200. * The writer can only be used inside these blocks which ensures that the model
  1201. * can only be changed during such "sessions".
  1202. *
  1203. * @error writer-incorrect-use
  1204. */
  1205. if ( this.model._currentWriter !== this ) {
  1206. throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.', this );
  1207. }
  1208. }
  1209. /**
  1210. * For given action `type` and `positionOrRange` where the action happens, this function finds all affected markers
  1211. * and applies a marker operation with the new marker range equal to the current range. Thanks to this, the marker range
  1212. * can be later correctly processed during undo.
  1213. *
  1214. * @private
  1215. * @param {'move'|'merge'} type Writer action type.
  1216. * @param {module:engine/model/position~Position|module:engine/model/range~Range} positionOrRange Position or range
  1217. * where the writer action happens.
  1218. */
  1219. _addOperationForAffectedMarkers( type, positionOrRange ) {
  1220. for ( const marker of this.model.markers ) {
  1221. if ( !marker.managedUsingOperations ) {
  1222. continue;
  1223. }
  1224. const markerRange = marker.getRange();
  1225. let isAffected = false;
  1226. if ( type === 'move' ) {
  1227. isAffected =
  1228. positionOrRange.containsPosition( markerRange.start ) ||
  1229. positionOrRange.start.isEqual( markerRange.start ) ||
  1230. positionOrRange.containsPosition( markerRange.end ) ||
  1231. positionOrRange.end.isEqual( markerRange.end );
  1232. } else {
  1233. // if type === 'merge'.
  1234. const elementBefore = positionOrRange.nodeBefore;
  1235. const elementAfter = positionOrRange.nodeAfter;
  1236. // Start: <p>Foo[</p><p>Bar]</p>
  1237. // After merge: <p>Foo[Bar]</p>
  1238. // After undoing split: <p>Foo</p><p>[Bar]</p> <-- incorrect, needs remembering for undo.
  1239. //
  1240. const affectedInLeftElement = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
  1241. // Start: <p>[Foo</p><p>]Bar</p>
  1242. // After merge: <p>[Foo]Bar</p>
  1243. // After undoing split: <p>[Foo]</p><p>Bar</p> <-- incorrect, needs remembering for undo.
  1244. //
  1245. const affectedInRightElement = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;
  1246. // Start: <p>[Foo</p>]<p>Bar</p>
  1247. // After merge: <p>[Foo]Bar</p>
  1248. // After undoing split: <p>[Foo]</p><p>Bar</p> <-- incorrect, needs remembering for undo.
  1249. //
  1250. const affectedAfterLeftElement = markerRange.end.nodeAfter == elementAfter;
  1251. // Start: <p>Foo</p>[<p>Bar]</p>
  1252. // After merge: <p>Foo[Bar]</p>
  1253. // After undoing split: <p>Foo</p><p>[Bar]</p> <-- incorrect, needs remembering for undo.
  1254. //
  1255. const affectedBeforeRightElement = markerRange.start.nodeAfter == elementAfter;
  1256. isAffected = affectedInLeftElement || affectedInRightElement || affectedAfterLeftElement || affectedBeforeRightElement;
  1257. }
  1258. if ( isAffected ) {
  1259. this.updateMarker( marker.name, { range: markerRange } );
  1260. }
  1261. }
  1262. }
  1263. }
  1264. // Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
  1265. //
  1266. // Because attribute operation needs to have the same attribute value on the whole range, this function splits
  1267. // the range into smaller parts.
  1268. //
  1269. // Given `range` must be flat.
  1270. //
  1271. // @private
  1272. // @param {module:engine/model/writer~Writer} writer
  1273. // @param {String} key Attribute key.
  1274. // @param {*} value Attribute new value.
  1275. // @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
  1276. function setAttributeOnRange( writer, key, value, range ) {
  1277. const model = writer.model;
  1278. const doc = model.document;
  1279. // Position of the last split, the beginning of the new range.
  1280. let lastSplitPosition = range.start;
  1281. // Currently position in the scanning range. Because we need value after the position, it is not a current
  1282. // position of the iterator but the previous one (we need to iterate one more time to get the value after).
  1283. let position;
  1284. // Value before the currently position.
  1285. let valueBefore;
  1286. // Value after the currently position.
  1287. let valueAfter;
  1288. for ( const val of range.getWalker( { shallow: true } ) ) {
  1289. valueAfter = val.item.getAttribute( key );
  1290. // At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
  1291. // because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
  1292. if ( position && valueBefore != valueAfter ) {
  1293. // if valueBefore == value there is nothing to change, so we add operation only if these values are different.
  1294. if ( valueBefore != value ) {
  1295. addOperation();
  1296. }
  1297. lastSplitPosition = position;
  1298. }
  1299. position = val.nextPosition;
  1300. valueBefore = valueAfter;
  1301. }
  1302. // Because position in the loop is not the iterator position (see let position comment), the last position in
  1303. // the while loop will be last but one position in the range. We need to check the last position manually.
  1304. if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
  1305. addOperation();
  1306. }
  1307. function addOperation() {
  1308. const range = new Range( lastSplitPosition, position );
  1309. const version = range.root.document ? doc.version : null;
  1310. const operation = new AttributeOperation( range, key, valueBefore, value, version );
  1311. writer.batch.addOperation( operation );
  1312. model.applyOperation( operation );
  1313. }
  1314. }
  1315. // Sets given attribute to the given node. When attribute value is null then attribute will be removed.
  1316. //
  1317. // @private
  1318. // @param {module:engine/model/writer~Writer} writer
  1319. // @param {String} key Attribute key.
  1320. // @param {*} value Attribute new value.
  1321. // @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
  1322. function setAttributeOnItem( writer, key, value, item ) {
  1323. const model = writer.model;
  1324. const doc = model.document;
  1325. const previousValue = item.getAttribute( key );
  1326. let range, operation;
  1327. if ( previousValue != value ) {
  1328. const isRootChanged = item.root === item;
  1329. if ( isRootChanged ) {
  1330. // If we change attributes of root element, we have to use `RootAttributeOperation`.
  1331. const version = item.document ? doc.version : null;
  1332. operation = new RootAttributeOperation( item, key, previousValue, value, version );
  1333. } else {
  1334. range = new Range( Position._createBefore( item ), writer.createPositionAfter( item ) );
  1335. const version = range.root.document ? doc.version : null;
  1336. operation = new AttributeOperation( range, key, previousValue, value, version );
  1337. }
  1338. writer.batch.addOperation( operation );
  1339. model.applyOperation( operation );
  1340. }
  1341. }
  1342. // Creates and applies marker operation to {@link module:engine/model/operation/operation~Operation operation}.
  1343. //
  1344. // @private
  1345. // @param {module:engine/model/writer~Writer} writer
  1346. // @param {String} name Marker name.
  1347. // @param {module:engine/model/range~Range} oldRange Marker range before the change.
  1348. // @param {module:engine/model/range~Range} newRange Marker range after the change.
  1349. // @param {Boolean} affectsData
  1350. function applyMarkerOperation( writer, name, oldRange, newRange, affectsData ) {
  1351. const model = writer.model;
  1352. const doc = model.document;
  1353. const operation = new MarkerOperation( name, oldRange, newRange, model.markers, affectsData, doc.version );
  1354. writer.batch.addOperation( operation );
  1355. model.applyOperation( operation );
  1356. }
  1357. // Creates `MoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
  1358. // The operation will be applied on given model instance and added to given operation instance.
  1359. //
  1360. // @private
  1361. // @param {module:engine/model/position~Position} position Position from which nodes are removed.
  1362. // @param {Number} howMany Number of nodes to remove.
  1363. // @param {Batch} batch Batch to which the operation will be added.
  1364. // @param {module:engine/model/model~Model} model Model instance on which operation will be applied.
  1365. function applyRemoveOperation( position, howMany, batch, model ) {
  1366. let operation;
  1367. if ( position.root.document ) {
  1368. const doc = model.document;
  1369. const graveyardPosition = new Position( doc.graveyard, [ 0 ] );
  1370. operation = new MoveOperation( position, howMany, graveyardPosition, doc.version );
  1371. } else {
  1372. operation = new DetachOperation( position, howMany );
  1373. }
  1374. batch.addOperation( operation );
  1375. model.applyOperation( operation );
  1376. }
  1377. // Returns `true` if both root elements are the same element or both are documents root elements.
  1378. //
  1379. // Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
  1380. // within the same document fragment), but when element supposed to be moved from document fragment to the document, or
  1381. // to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
  1382. // collaboration may track changes on the document but ignore changes on detached fragments and should not get
  1383. // unexpected `move` operation.
  1384. function isSameTree( rootA, rootB ) {
  1385. // If it is the same root this is the same tree.
  1386. if ( rootA === rootB ) {
  1387. return true;
  1388. }
  1389. // If both roots are documents root it is operation within the document what we still treat as the same tree.
  1390. if ( rootA instanceof RootElement && rootB instanceof RootElement ) {
  1391. return true;
  1392. }
  1393. return false;
  1394. }