writer.js 55 KB

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