writer.js 51 KB

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