| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module engine/model/writer
- */
- import AttributeOperation from './operation/attributeoperation';
- import DetachOperation from './operation/detachoperation';
- import InsertOperation from './operation/insertoperation';
- import MarkerOperation from './operation/markeroperation';
- import MoveOperation from './operation/moveoperation';
- import RenameOperation from './operation/renameoperation';
- import RootAttributeOperation from './operation/rootattributeoperation';
- import SplitOperation from './operation/splitoperation';
- import MergeOperation from './operation/mergeoperation';
- import WrapOperation from './operation/wrapoperation';
- import UnwrapOperation from './operation/unwrapoperation';
- import DocumentFragment from './documentfragment';
- import Text from './text';
- import Element from './element';
- import RootElement from './rootelement';
- import Position from './position';
- import Range from './range.js';
- import DocumentSelection from './documentselection';
- import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
- /**
- * The model can only be modified by using the writer. It should be used whenever you want to create a node, modify
- * child nodes, attributes or text, set the selection's position and its attributes.
- *
- * The instance of the writer is only available in the {@link module:engine/model/model~Model#change `change()`} or
- * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`}.
- *
- * model.change( writer => {
- * writer.insertText( 'foo', paragraph, 'end' );
- * } );
- *
- * Note that the writer should never be stored and used outside of the `change()` and
- * `enqueueChange()` blocks.
- *
- * @see module:engine/model/model~Model#change
- * @see module:engine/model/model~Model#enqueueChange
- */
- export default class Writer {
- /**
- * Creates a writer instance.
- *
- * **Note:** It is not recommended to use it directly. Use {@link module:engine/model/model~Model#change `Model#change()`} or
- * {@link module:engine/model/model~Model#enqueueChange `Model#enqueueChange()`} instead.
- *
- * @protected
- * @param {module:engine/model/model~Model} model
- * @param {module:engine/model/batch~Batch} batch
- */
- constructor( model, batch ) {
- /**
- * Instance of the model on which this writer operates.
- *
- * @readonly
- * @type {module:engine/model/model~Model}
- */
- this.model = model;
- /**
- * The batch to which this writer will add changes.
- *
- * @readonly
- * @type {module:engine/model/batch~Batch}
- */
- this.batch = batch;
- }
- /**
- * Creates a new {@link module:engine/model/text~Text text node}.
- *
- * writer.createText( 'foo' );
- * writer.createText( 'foo', { bold: true } );
- *
- * @param {String} data Text data.
- * @param {Object} [attributes] Text attributes.
- * @returns {module:engine/model/text~Text} Created text node.
- */
- createText( data, attributes ) {
- return new Text( data, attributes );
- }
- /**
- * Creates a new {@link module:engine/model/element~Element element}.
- *
- * writer.createElement( 'paragraph' );
- * writer.createElement( 'paragraph', { alignment: 'center' } );
- *
- * @param {String} name Name of the element.
- * @param {Object} [attributes] Elements attributes.
- * @returns {module:engine/model/element~Element} Created element.
- */
- createElement( name, attributes ) {
- return new Element( name, attributes );
- }
- /**
- * Creates a new {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
- *
- * @returns {module:engine/model/documentfragment~DocumentFragment} Created document fragment.
- */
- createDocumentFragment() {
- return new DocumentFragment();
- }
- /**
- * Inserts item on given position.
- *
- * const paragraph = writer.createElement( 'paragraph' );
- * writer.insert( paragraph, position );
- *
- * Instead of using position you can use parent and offset:
- *
- * const text = writer.createText( 'foo' );
- * writer.insert( text, paragraph, 5 );
- *
- * You can also use `end` instead of the offset to insert at the end:
- *
- * const text = writer.createText( 'foo' );
- * writer.insert( text, paragraph, 'end' );
- *
- * Or insert before or after another element:
- *
- * const paragraph = writer.createElement( 'paragraph' );
- * writer.insert( paragraph, anotherParagraph, 'after' );
- *
- * These parameters works the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
- *
- * Note that if the item already has parent it will be removed from the previous parent.
- *
- * Note that you cannot re-insert a node from a document to a different document or a document fragment. In this case,
- * `model-writer-insert-forbidden-move` is thrown.
- *
- * If you want to move {@link module:engine/model/range~Range range} instead of an
- * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
- *
- * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} item Item or document
- * fragment to insert.
- * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
- * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
- * second parameter is a {@link module:engine/model/item~Item model item}.
- */
- insert( item, itemOrPosition, offset ) {
- this._assertWriterUsedCorrectly();
- const position = Position.createAt( itemOrPosition, offset );
- // If item has a parent already.
- if ( item.parent ) {
- // We need to check if item is going to be inserted within the same document.
- if ( isSameTree( item.root, position.root ) ) {
- // If it's we just need to move it.
- this.move( Range.createOn( item ), position );
- return;
- }
- // If it isn't the same root.
- else {
- if ( item.root.document ) {
- // It is forbidden to move a node that was already in a document outside of it.
- throw new Error( 'model-writer-insert-forbidden-move: Cannot move a node from a document to a different tree.' );
- } else {
- // Move between two different document fragments or from document fragment to a document is possible.
- // In that case, remove the item from it's original parent.
- this.remove( item );
- }
- }
- }
- const version = position.root.document ? position.root.document.version : null;
- const insert = new InsertOperation( position, item, version );
- if ( item instanceof Text ) {
- insert.shouldReceiveAttributes = true;
- }
- this.batch.addOperation( insert );
- this.model.applyOperation( insert );
- // When element is a DocumentFragment we need to move its markers to Document#markers.
- if ( item instanceof DocumentFragment ) {
- for ( const [ markerName, markerRange ] of item.markers ) {
- // We need to migrate marker range from DocumentFragment to Document.
- const rangeRootPosition = Position.createAt( markerRange.root );
- const range = new Range(
- markerRange.start._getCombined( rangeRootPosition, position ),
- markerRange.end._getCombined( rangeRootPosition, position )
- );
- this.addMarker( markerName, { range, usingOperation: true } );
- }
- }
- }
- /**
- * Creates and inserts text on given position. You can optionally set text attributes:
- *
- * writer.insertText( 'foo', position );
- * writer.insertText( 'foo', { bold: true }, position );
- *
- * Instead of using position you can use parent and offset or define that text should be inserted at the end
- * or before or after other node:
- *
- * // Inserts 'foo' in paragraph, at offset 5:
- * writer.insertText( 'foo', paragraph, 5 );
- * // Inserts 'foo' at the end of a paragraph:
- * writer.insertText( 'foo', paragraph, 'end' );
- * // Inserts 'foo' after an image:
- * writer.insertText( 'foo', image, 'after' );
- *
- * These parameters work in the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
- *
- * @param {String} data Text data.
- * @param {Object} [attributes] Text attributes.
- * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
- * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
- * third parameter is a {@link module:engine/model/item~Item model item}.
- */
- insertText( text, attributes, itemOrPosition, offset ) {
- if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
- this.insert( this.createText( text ), attributes, itemOrPosition );
- } else {
- this.insert( this.createText( text, attributes ), itemOrPosition, offset );
- }
- }
- /**
- * Creates and inserts element on given position. You can optionally set attributes:
- *
- * writer.insertElement( 'paragraph', position );
- * writer.insertElement( 'paragraph', { alignment: 'center' }, position );
- *
- * Instead of using position you can use parent and offset or define that text should be inserted at the end
- * or before or after other node:
- *
- * // Inserts paragraph in the root at offset 5:
- * writer.insertElement( 'paragraph', root, 5 );
- * // Inserts paragraph at the end of a blockquote:
- * writer.insertElement( 'paragraph', blockquote, 'end' );
- * // Inserts after an image:
- * writer.insertElement( 'paragraph', image, 'after' );
- *
- * These parameters works the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
- *
- * @param {String} name Name of the element.
- * @param {Object} [attributes] Elements attributes.
- * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
- * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
- * third parameter is a {@link module:engine/model/item~Item model item}.
- */
- insertElement( name, attributes, itemOrPosition, offset ) {
- if ( attributes instanceof DocumentFragment || attributes instanceof Element || attributes instanceof Position ) {
- this.insert( this.createElement( name ), attributes, itemOrPosition );
- } else {
- this.insert( this.createElement( name, attributes ), itemOrPosition, offset );
- }
- }
- /**
- * Inserts item at the end of the given parent.
- *
- * const paragraph = writer.createElement( 'paragraph' );
- * writer.append( paragraph, root );
- *
- * Note that if the item already has parent it will be removed from the previous parent.
- *
- * If you want to move {@link module:engine/model/range~Range range} instead of an
- * {@link module:engine/model/item~Item item} use {@link module:engine/model/writer~Writer#move `Writer#move()`}.
- *
- * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment}
- * item Item or document fragment to insert.
- * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
- */
- append( item, parent ) {
- this.insert( item, parent, 'end' );
- }
- /**
- * Creates text node and inserts it at the end of the parent. You can optionally set text attributes:
- *
- * writer.appendText( 'foo', paragraph );
- * writer.appendText( 'foo', { bold: true }, paragraph );
- *
- * @param {String} text Text data.
- * @param {Object} [attributes] Text attributes.
- * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
- */
- appendText( text, attributes, parent ) {
- if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
- this.insert( this.createText( text ), attributes, 'end' );
- } else {
- this.insert( this.createText( text, attributes ), parent, 'end' );
- }
- }
- /**
- * Creates element and inserts it at the end of the parent. You can optionally set attributes:
- *
- * writer.appendElement( 'paragraph', root );
- * writer.appendElement( 'paragraph', { alignment: 'center' }, root );
- *
- * @param {String} name Name of the element.
- * @param {Object} [attributes] Elements attributes.
- * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent
- */
- appendElement( name, attributes, parent ) {
- if ( attributes instanceof DocumentFragment || attributes instanceof Element ) {
- this.insert( this.createElement( name ), attributes, 'end' );
- } else {
- this.insert( this.createElement( name, attributes ), parent, 'end' );
- }
- }
- /**
- * Sets value of the attribute with given key on a {@link module:engine/model/item~Item model item}
- * or on a {@link module:engine/model/range~Range range}.
- *
- * @param {String} key Attribute key.
- * @param {*} value Attribute new value.
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range on which the attribute will be set.
- */
- setAttribute( key, value, itemOrRange ) {
- this._assertWriterUsedCorrectly();
- if ( itemOrRange instanceof Range ) {
- const ranges = itemOrRange.getMinimalFlatRanges();
- for ( const range of ranges ) {
- setAttributeOnRange( this, key, value, range );
- }
- } else {
- setAttributeOnItem( this, key, value, itemOrRange );
- }
- }
- /**
- * Sets values of attributes on a {@link module:engine/model/item~Item model item}
- * or on a {@link module:engine/model/range~Range range}.
- *
- * writer.setAttributes( {
- * bold: true,
- * italic: true
- * }, range );
- *
- * @param {Object} attributes Attributes keys and values.
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range on which the attributes will be set.
- */
- setAttributes( attributes, itemOrRange ) {
- for ( const [ key, val ] of toMap( attributes ) ) {
- this.setAttribute( key, val, itemOrRange );
- }
- }
- /**
- * Removes an attribute with given key from a {@link module:engine/model/item~Item model item}
- * or from a {@link module:engine/model/range~Range range}.
- *
- * @param {String} key Attribute key.
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range from which the attribute will be removed.
- */
- removeAttribute( key, itemOrRange ) {
- this._assertWriterUsedCorrectly();
- if ( itemOrRange instanceof Range ) {
- const ranges = itemOrRange.getMinimalFlatRanges();
- for ( const range of ranges ) {
- setAttributeOnRange( this, key, null, range );
- }
- } else {
- setAttributeOnItem( this, key, null, itemOrRange );
- }
- }
- /**
- * Removes all attributes from all elements in the range or from the given item.
- *
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange
- * Model item or range from which all attributes will be removed.
- */
- clearAttributes( itemOrRange ) {
- this._assertWriterUsedCorrectly();
- const removeAttributesFromItem = item => {
- for ( const attribute of item.getAttributeKeys() ) {
- this.removeAttribute( attribute, item );
- }
- };
- if ( !( itemOrRange instanceof Range ) ) {
- removeAttributesFromItem( itemOrRange );
- } else {
- for ( const item of itemOrRange.getItems() ) {
- removeAttributesFromItem( item );
- }
- }
- }
- /**
- * Moves all items in the source range to the target position.
- *
- * writer.move( sourceRange, targetPosition );
- *
- * Instead of the target position you can use parent and offset or define that range should be moved to the end
- * or before or after chosen item:
- *
- * // Moves all items in the range to the paragraph at offset 5:
- * writer.move( sourceRange, paragraph, 5 );
- * // Moves all items in the range to the end of a blockquote:
- * writer.move( sourceRange, blockquote, 'end' );
- * // Moves all items in the range to a position after an image:
- * writer.move( sourceRange, image, 'after' );
- *
- * These parameters works the same way as {@link module:engine/model/position~Position.createAt `Position.createAt()`}.
- *
- * Note that items can be moved only within the same tree. It means that you can move items within the same root
- * (element or document fragment) or between {@link module:engine/model/document~Document#roots documents roots},
- * but you can not move items from document fragment to the document or from one detached element to another. Use
- * {@link module:engine/model/writer~Writer#insert} in such cases.
- *
- * @param {module:engine/model/range~Range} range Source range.
- * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
- * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
- * second parameter is a {@link module:engine/model/item~Item model item}.
- */
- move( range, itemOrPosition, offset ) {
- this._assertWriterUsedCorrectly();
- if ( !( range instanceof Range ) ) {
- /**
- * Invalid range to move.
- *
- * @error writer-move-invalid-range
- */
- throw new CKEditorError( 'writer-move-invalid-range: Invalid range to move.' );
- }
- if ( !range.isFlat ) {
- /**
- * Range to move is not flat.
- *
- * @error writer-move-range-not-flat
- */
- throw new CKEditorError( 'writer-move-range-not-flat: Range to move is not flat.' );
- }
- const position = Position.createAt( itemOrPosition, offset );
- if ( !isSameTree( range.root, position.root ) ) {
- /**
- * Range is going to be moved within not the same document. Please use
- * {@link module:engine/model/writer~Writer#insert insert} instead.
- *
- * @error writer-move-different-document
- */
- throw new CKEditorError( 'writer-move-different-document: Range is going to be moved between different documents.' );
- }
- const version = range.root.document ? range.root.document.version : null;
- const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, version );
- this.batch.addOperation( operation );
- this.model.applyOperation( operation );
- }
- /**
- * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}.
- *
- * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove.
- */
- remove( itemOrRange ) {
- this._assertWriterUsedCorrectly();
- if ( itemOrRange instanceof Range ) {
- // The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
- const ranges = itemOrRange.getMinimalFlatRanges().reverse();
- for ( const flat of ranges ) {
- applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model );
- }
- } else {
- const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
- applyRemoveOperation( Position.createBefore( itemOrRange ), howMany, this.batch, this.model );
- }
- }
- /**
- * Merges two siblings at the given position.
- *
- * Node before and after the position have to be an element. Otherwise `writer-merge-no-element-before` or
- * `writer-merge-no-element-after` error will be thrown.
- *
- * @param {module:engine/model/position~Position} position Position between merged elements.
- */
- merge( position ) {
- this._assertWriterUsedCorrectly();
- const nodeBefore = position.nodeBefore;
- const nodeAfter = position.nodeAfter;
- if ( !( nodeBefore instanceof Element ) ) {
- /**
- * Node before merge position must be an element.
- *
- * @error writer-merge-no-element-before
- */
- throw new CKEditorError( 'writer-merge-no-element-before: Node before merge position must be an element.' );
- }
- if ( !( nodeAfter instanceof Element ) ) {
- /**
- * Node after merge position must be an element.
- *
- * @error writer-merge-no-element-after
- */
- throw new CKEditorError( 'writer-merge-no-element-after: Node after merge position must be an element.' );
- }
- if ( !position.root.document ) {
- this._mergeDetached( position );
- } else {
- this._merge( position );
- }
- }
- /**
- * Performs merge action in a detached tree.
- *
- * @private
- */
- _mergeDetached( position ) {
- const nodeBefore = position.nodeBefore;
- const nodeAfter = position.nodeAfter;
- this.move( Range.createIn( nodeAfter ), Position.createAt( nodeBefore, 'end' ) );
- this.remove( nodeAfter );
- }
- /**
- * Performs merge action in a non-detached tree.
- *
- * @private
- * @param {module:engine/model/position~Position} position Position between merged elements.
- */
- _merge( position ) {
- const targetPosition = Position.createAt( position.nodeBefore, 'end' );
- const sourcePosition = Position.createAt( position.nodeAfter, 0 );
- const graveyard = position.root.document.graveyard;
- const graveyardPosition = new Position( graveyard, [ 0 ] );
- const version = position.root.document.version;
- const merge = new MergeOperation( sourcePosition, position.nodeAfter.maxOffset, targetPosition, graveyardPosition, version );
- this.batch.addOperation( merge );
- this.model.applyOperation( merge );
- }
- /**
- * Renames the given element.
- *
- * @param {module:engine/model/element~Element} element The element to rename.
- * @param {String} newName New element name.
- */
- rename( element, newName ) {
- this._assertWriterUsedCorrectly();
- if ( !( element instanceof Element ) ) {
- /**
- * Trying to rename an object which is not an instance of Element.
- *
- * @error writer-rename-not-element-instance
- */
- throw new CKEditorError(
- 'writer-rename-not-element-instance: Trying to rename an object which is not an instance of Element.'
- );
- }
- const version = element.root.document ? element.root.document.version : null;
- const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, version );
- this.batch.addOperation( renameOperation );
- this.model.applyOperation( renameOperation );
- }
- /**
- * Splits elements starting from the given position and going to the top of the model tree as long as given
- * `limitElement` is reached. When `limitElement` is not defined then only the parent of the given position will be split.
- *
- * The element needs to have a parent. It cannot be a root element nor a document fragment.
- * The `writer-split-element-no-parent` error will be thrown if you try to split an element with no parent.
- *
- * @param {module:engine/model/position~Position} position Position of split.
- * @param {module:engine/model/node~Node} [limitElement] Stop splitting when this element will be reached.
- * @returns {Object} result Split result.
- * @returns {module:engine/model/position~Position} result.position between split elements.
- * @returns {module:engine/model/range~Range} result.range Range that stars from the end of the first split element and ands
- * at the beginning of the first copy element.
- */
- split( position, limitElement ) {
- this._assertWriterUsedCorrectly();
- let splitElement = position.parent;
- if ( !splitElement.parent ) {
- /**
- * Element with no parent can not be split.
- *
- * @error writer-split-element-no-parent
- */
- throw new CKEditorError( 'writer-split-element-no-parent: Element with no parent can not be split.' );
- }
- // When limit element is not defined lets set splitElement parent as limit.
- if ( !limitElement ) {
- limitElement = splitElement.parent;
- }
- if ( !position.parent.getAncestors( { includeSelf: true } ).includes( limitElement ) ) {
- throw new CKEditorError( 'writer-split-invalid-limit-element: Limit element is not a position ancestor.' );
- }
- // We need to cache elements that will be created as a result of the first split because
- // we need to create a range from the end of the first split element to the beginning of the
- // first copy element. This should be handled by LiveRange but it doesn't work on detached nodes.
- let firstSplitElement, firstCopyElement;
- do {
- const version = splitElement.root.document ? splitElement.root.document.version : null;
- const howMany = splitElement.maxOffset - position.offset;
- const split = new SplitOperation( position, howMany, null, version );
- this.batch.addOperation( split );
- this.model.applyOperation( split );
- // Cache result of the first split.
- if ( !firstSplitElement && !firstCopyElement ) {
- firstSplitElement = splitElement;
- firstCopyElement = position.parent.nextSibling;
- }
- position = Position.createAfter( position.parent );
- splitElement = position.parent;
- } while ( splitElement !== limitElement );
- return {
- position,
- range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement ) )
- };
- }
- /**
- * Wraps the given range with the given element or with a new element (if a string was passed).
- *
- * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat `Range#isFlat`}).
- * If not, an error will be thrown.
- *
- * @param {module:engine/model/range~Range} range Range to wrap.
- * @param {module:engine/model/element~Element|String} elementOrString Element or name of element to wrap the range with.
- */
- wrap( range, elementOrString ) {
- this._assertWriterUsedCorrectly();
- if ( !range.isFlat ) {
- /**
- * Range to wrap is not flat.
- *
- * @error writer-wrap-range-not-flat
- */
- throw new CKEditorError( 'writer-wrap-range-not-flat: Range to wrap is not flat.' );
- }
- const element = elementOrString instanceof Element ? elementOrString : new Element( elementOrString );
- if ( element.childCount > 0 ) {
- /**
- * Element to wrap with is not empty.
- *
- * @error writer-wrap-element-not-empty
- */
- throw new CKEditorError( 'writer-wrap-element-not-empty: Element to wrap with is not empty.' );
- }
- if ( element.parent !== null ) {
- /**
- * Element to wrap with is already attached to a tree model.
- *
- * @error writer-wrap-element-attached
- */
- throw new CKEditorError( 'writer-wrap-element-attached: Element to wrap with is already attached to tree model.' );
- }
- const version = range.root.document ? range.root.document.version : null;
- const wrap = new WrapOperation( range.start, range.end.offset - range.start.offset, element, version );
- this.batch.addOperation( wrap );
- this.model.applyOperation( wrap );
- }
- /**
- * Unwraps children of the given element – all its children are moved before it and then the element is removed.
- * Throws error if you try to unwrap an element which does not have a parent.
- *
- * @param {module:engine/model/element~Element} element Element to unwrap.
- */
- unwrap( element ) {
- this._assertWriterUsedCorrectly();
- if ( element.parent === null ) {
- /**
- * Trying to unwrap an element which has no parent.
- *
- * @error writer-unwrap-element-no-parent
- */
- throw new CKEditorError( 'writer-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
- }
- if ( !element.root.document ) {
- this._unwrapDetached( element );
- } else {
- this._unwrap( element );
- }
- }
- /**
- * Performs unwrap action in a detached tree.
- *
- * @private
- * @param {module:engine/model/element~Element} element Element to unwrap.
- */
- _unwrapDetached( element ) {
- this.move( Range.createIn( element ), Position.createAfter( element ) );
- this.remove( element );
- }
- /**
- * Performs unwrap action in a non-detached tree.
- *
- * @private
- * @param {module:engine/model/element~Element} element Element to unwrap.
- */
- _unwrap( element ) {
- const position = Position.createAt( element, 0 );
- const version = position.root.document.version;
- const graveyard = position.root.document.graveyard;
- const graveyardPosition = new Position( graveyard, [ 0 ] );
- const unwrap = new UnwrapOperation( position, element.maxOffset, graveyardPosition, version );
- this.batch.addOperation( unwrap );
- this.model.applyOperation( unwrap );
- }
- /**
- * Adds a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
- * changes in the document and updates its range automatically, when model tree changes.
- *
- * As the first parameter you can set marker name.
- *
- * The required `options.usingOperation` parameter lets you decide if the marker should be managed by operations or not. See
- * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
- * markers managed by operations and not-managed by operations.
- *
- * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
- * `true` when the marker change changes the data returned by the
- * {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
- * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
- * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
- *
- * Create marker directly base on marker's name:
- *
- * addMarker( markerName, { range, usingOperation: false } );
- *
- * Create marker using operation:
- *
- * addMarker( markerName, { range, usingOperation: true } );
- *
- * Create marker that affects the editor data:
- *
- * addMarker( markerName, { range, usingOperation: false, affectsData: true } );
- *
- * Note: For efficiency reasons, it's best to create and keep as little markers as possible.
- *
- * @see module:engine/model/markercollection~Marker
- * @param {String} name Name of a marker to create - must be unique.
- * @param {Object} options
- * @param {Boolean} options.usingOperation Flag indicating that the marker should be added by MarkerOperation.
- * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
- * @param {module:engine/model/range~Range} options.range Marker range.
- * @param {Boolean} [options.affectsData=false] Flag indicating that the marker changes the editor data.
- * @returns {module:engine/model/markercollection~Marker} Marker that was set.
- */
- addMarker( name, options ) {
- this._assertWriterUsedCorrectly();
- if ( !options || typeof options.usingOperation != 'boolean' ) {
- /**
- * The `options.usingOperations` parameter is required when adding a new marker.
- *
- * @error writer-addMarker-no-usingOperations
- */
- throw new CKEditorError(
- 'writer-addMarker-no-usingOperations: The options.usingOperations parameter is required when adding a new marker.'
- );
- }
- const usingOperation = options.usingOperation;
- const range = options.range;
- const affectsData = options.affectsData === undefined ? false : options.affectsData;
- if ( this.model.markers.has( name ) ) {
- /**
- * Marker with provided name already exists.
- *
- * @error writer-addMarker-marker-exists
- */
- throw new CKEditorError( 'writer-addMarker-marker-exists: Marker with provided name already exists.' );
- }
- if ( !range ) {
- /**
- * Range parameter is required when adding a new marker.
- *
- * @error writer-addMarker-no-range
- */
- throw new CKEditorError( 'writer-addMarker-no-range: Range parameter is required when adding a new marker.' );
- }
- if ( !usingOperation ) {
- return this.model.markers._set( name, range, usingOperation, affectsData );
- }
- applyMarkerOperation( this, name, null, range, affectsData );
- return this.model.markers.get( name );
- }
- /**
- * Adds or updates a {@link module:engine/model/markercollection~Marker marker}. Marker is a named range, which tracks
- * changes in the document and updates its range automatically, when model tree changes. Still, it is possible to change the
- * marker's range directly using this method.
- *
- * As the first parameter you can set marker name or instance. If none of them is provided, new marker, with a unique
- * name is created and returned.
- *
- * The `options.usingOperation` parameter lets you change if the marker should be managed by operations or not. See
- * {@link module:engine/model/markercollection~Marker marker class description} to learn about the difference between
- * markers managed by operations and not-managed by operations. It is possible to change this option for an existing marker.
- *
- * The `options.affectsData` parameter, which defaults to `false`, allows you to define if a marker affects the data. It should be
- * `true` when the marker change changes the data returned by
- * the {@link module:core/editor/utils/dataapimixin~DataApi#getData `editor.getData()`} method.
- * When set to `true` it fires the {@link module:engine/model/document~Document#event:change:data `change:data`} event.
- * When set to `false` it fires the {@link module:engine/model/document~Document#event:change `change`} event.
- *
- * Update marker directly base on marker's name:
- *
- * updateMarker( markerName, { range } );
- *
- * Update marker using operation:
- *
- * updateMarker( marker, { range, usingOperation: true } );
- * updateMarker( markerName, { range, usingOperation: true } );
- *
- * Change marker's option (start using operations to manage it):
- *
- * updateMarker( marker, { usingOperation: true } );
- *
- * Change marker's option (inform the engine, that the marker does not affect the data anymore):
- *
- * updateMarker( markerName, { affectsData: false } );
- *
- * @see module:engine/model/markercollection~Marker
- * @param {String} markerOrName Name of a marker to update, or a marker instance.
- * @param {Object} options
- * @param {module:engine/model/range~Range} [options.range] Marker range to update.
- * @param {Boolean} [options.usingOperation] Flag indicated whether the marker should be added by MarkerOperation.
- * See {@link module:engine/model/markercollection~Marker#managedUsingOperations}.
- * @param {Boolean} [options.affectsData] Flag indicating that the marker changes the editor data.
- */
- updateMarker( markerOrName, options = {} ) {
- this._assertWriterUsedCorrectly();
- const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
- const currentMarker = this.model.markers.get( markerName );
- if ( !currentMarker ) {
- /**
- * Marker with provided name does not exists.
- *
- * @error writer-updateMarker-marker-not-exists
- */
- throw new CKEditorError( 'writer-updateMarker-marker-not-exists: Marker with provided name does not exists.' );
- }
- const hasUsingOperationDefined = typeof options.usingOperation == 'boolean';
- const affectsDataDefined = typeof options.affectsData == 'boolean';
- // Use previously defined marker's affectsData if the property is not provided.
- const affectsData = affectsDataDefined ? options.affectsData : currentMarker.affectsData;
- if ( !hasUsingOperationDefined && !options.range && !affectsDataDefined ) {
- /**
- * One of the options is required - provide range, usingOperations or affectsData.
- *
- * @error writer-updateMarker-wrong-options
- */
- throw new CKEditorError(
- 'writer-updateMarker-wrong-options: One of the options is required - provide range, usingOperations or affectsData.'
- );
- }
- const currentRange = currentMarker.getRange();
- const updatedRange = options.range ? options.range : currentRange;
- if ( hasUsingOperationDefined && options.usingOperation !== currentMarker.managedUsingOperations ) {
- // The marker type is changed so it's necessary to create proper operations.
- if ( options.usingOperation ) {
- // If marker changes to a managed one treat this as synchronizing existing marker.
- // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
- applyMarkerOperation( this, markerName, null, updatedRange, affectsData );
- } else {
- // If marker changes to a marker that do not use operations then we need to create additional operation
- // that removes that marker first.
- applyMarkerOperation( this, markerName, currentRange, null, affectsData );
- // Although not managed the marker itself should stay in model and its range should be preserver or changed to passed range.
- this.model.markers._set( markerName, updatedRange, undefined, affectsData );
- }
- return;
- }
- // Marker's type doesn't change so update it accordingly.
- if ( currentMarker.managedUsingOperations ) {
- applyMarkerOperation( this, markerName, currentRange, updatedRange, affectsData );
- } else {
- this.model.markers._set( markerName, updatedRange, undefined, affectsData );
- }
- }
- /**
- * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
- * The marker is removed accordingly to how it has been created, so if the marker was created using operation,
- * it will be destroyed using operation.
- *
- * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
- */
- removeMarker( markerOrName ) {
- this._assertWriterUsedCorrectly();
- const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
- if ( !this.model.markers.has( name ) ) {
- /**
- * Trying to remove marker which does not exist.
- *
- * @error writer-removeMarker-no-marker
- */
- throw new CKEditorError( 'writer-removeMarker-no-marker: Trying to remove marker which does not exist.' );
- }
- const marker = this.model.markers.get( name );
- if ( !marker.managedUsingOperations ) {
- this.model.markers._remove( name );
- return;
- }
- const oldRange = marker.getRange();
- applyMarkerOperation( this, name, oldRange, null, marker.affectsData );
- }
- /**
- * Sets this selection's ranges and direction to the specified location based on the given
- * {@link module:engine/model/selection~Selection selection}, {@link module:engine/model/position~Position position},
- * {@link module:engine/model/node~Node node}, {@link module:engine/model/position~Position position},
- * {@link module:engine/model/range~Range range}, an iterable of {@link module:engine/model/range~Range ranges} or null.
- *
- * // Sets selection to the given range.
- * const range = new Range( start, end );
- * writer.setSelection( range );
- *
- * // Sets selection to given ranges.
- * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
- * writer.setSelection( range );
- *
- * // Sets selection to other selection.
- * const otherSelection = new Selection();
- * writer.setSelection( otherSelection );
- *
- * // Sets selection to the given document selection.
- * const documentSelection = new DocumentSelection( doc );
- * writer.setSelection( documentSelection );
- *
- * // Sets collapsed selection at the given position.
- * const position = new Position( root, path );
- * writer.setSelection( position );
- *
- * // Sets collapsed selection at the position of the given node and an offset.
- * writer.setSelection( paragraph, offset );
- *
- * Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
- * that element and ends after the last child of that element.
- *
- * writer.setSelection( paragraph, 'in' );
- *
- * Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends just after the item.
- *
- * writer.setSelection( paragraph, 'on' );
- *
- * // Removes all selection's ranges.
- * writer.setSelection( null );
- *
- * `Writer#setSelection()` allow passing additional options (`backward`) as the last argument.
- *
- * // Sets selection as backward.
- * writer.setSelection( range, { backward: true } );
- *
- * Throws `writer-incorrect-use` error when the writer is used outside the `change()` block.
- *
- * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
- * module:engine/model/position~Position|module:engine/model/node~Node|
- * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} selectable
- * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
- * @param {Object} [options]
- * @param {Boolean} [options.backward] Sets this selection instance to be backward.
- */
- setSelection( selectable, placeOrOffset, options ) {
- this._assertWriterUsedCorrectly();
- this.model.document.selection._setTo( selectable, placeOrOffset, options );
- }
- /**
- * Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
- *
- * The location can be specified in the same form as
- * {@link module:engine/model/position~Position.createAt `Position.createAt()`} parameters.
- *
- * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
- * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
- * first parameter is a {@link module:engine/model/item~Item model item}.
- */
- setSelectionFocus( itemOrPosition, offset ) {
- this._assertWriterUsedCorrectly();
- this.model.document.selection._setFocus( itemOrPosition, offset );
- }
- /**
- * Sets attribute(s) on the selection. If attribute with the same key already is set, it's value is overwritten.
- *
- * Using key and value pair:
- *
- * writer.setSelectionAttribute( 'italic', true );
- *
- * Using key-value object:
- *
- * writer.setSelectionAttribute( { italic: true, bold: false } );
- *
- * Using iterable object:
- *
- * writer.setSelectionAttribute( new Map( [ [ 'italic', true ] ] ) );
- *
- * @param {String|Object|Iterable.<*>} keyOrObjectOrIterable Key of the attribute to set
- * or object / iterable of key => value attribute pairs.
- * @param {*} [value] Attribute value.
- */
- setSelectionAttribute( keyOrObjectOrIterable, value ) {
- this._assertWriterUsedCorrectly();
- if ( typeof keyOrObjectOrIterable === 'string' ) {
- this._setSelectionAttribute( keyOrObjectOrIterable, value );
- } else {
- for ( const [ key, value ] of toMap( keyOrObjectOrIterable ) ) {
- this._setSelectionAttribute( key, value );
- }
- }
- }
- /**
- * Removes attribute(s) with given key(s) from the selection.
- *
- * Remove one attribute:
- *
- * writer.removeSelectionAttribute( 'italic' );
- *
- * Remove multiple attributes:
- *
- * writer.removeSelectionAttribute( [ 'italic', 'bold' ] );
- *
- * @param {String|Iterable.<String>} keyOrIterableOfKeys Key of the attribute to remove or an iterable of attribute keys to remove.
- */
- removeSelectionAttribute( keyOrIterableOfKeys ) {
- this._assertWriterUsedCorrectly();
- if ( typeof keyOrIterableOfKeys === 'string' ) {
- this._removeSelectionAttribute( keyOrIterableOfKeys );
- } else {
- for ( const key of keyOrIterableOfKeys ) {
- this._removeSelectionAttribute( key );
- }
- }
- }
- /**
- * Temporarily changes the {@link module:engine/model/documentselection~DocumentSelection#isGravityOverridden gravity}
- * of the selection from left to right.
- *
- * The gravity defines from which direction the selection inherits its attributes. If it's the default left gravity,
- * then the selection (after being moved by the user) inherits attributes from its left-hand side.
- * This method allows to temporarily override this behavior by forcing the gravity to the right.
- *
- * For the following model fragment:
- *
- * <$text bold="true" linkHref="url">bar[]</$text><$text bold="true">biz</$text>
- *
- * * Default gravity: selection will have the `bold` and `linkHref` attributes.
- * * Overridden gravity: selection will have `bold` attribute.
- *
- * **Note**: It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
- * of the process.
- *
- * @returns {String} The unique id which allows restoring the gravity.
- */
- overrideSelectionGravity() {
- return this.model.document.selection._overrideGravity();
- }
- /**
- * Restores {@link ~Writer#overrideSelectionGravity} gravity to default.
- *
- * Restoring the gravity is only possible using the unique identifier returned by
- * {@link ~Writer#overrideSelectionGravity}. Note that the gravity remains overridden as long as won't be restored
- * the same number of times it was overridden.
- *
- * @param {String} uid The unique id returned by {@link ~Writer#overrideSelectionGravity}.
- */
- restoreSelectionGravity( uid ) {
- this.model.document.selection._restoreGravity( uid );
- }
- /**
- * @private
- * @param {String} key Key of the attribute to remove.
- * @param {*} value Attribute value.
- */
- _setSelectionAttribute( key, value ) {
- const selection = this.model.document.selection;
- // Store attribute in parent element if the selection is collapsed in an empty node.
- if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
- const storeKey = DocumentSelection._getStoreAttributeKey( key );
- this.setAttribute( storeKey, value, selection.anchor.parent );
- }
- selection._setAttribute( key, value );
- }
- /**
- * @private
- * @param {String} key Key of the attribute to remove.
- */
- _removeSelectionAttribute( key ) {
- const selection = this.model.document.selection;
- // Remove stored attribute from parent element if the selection is collapsed in an empty node.
- if ( selection.isCollapsed && selection.anchor.parent.isEmpty ) {
- const storeKey = DocumentSelection._getStoreAttributeKey( key );
- this.removeAttribute( storeKey, selection.anchor.parent );
- }
- selection._removeAttribute( key );
- }
- /**
- * Throws `writer-detached-writer-tries-to-modify-model` error when the writer is used outside of the `change()` block.
- *
- * @private
- */
- _assertWriterUsedCorrectly() {
- /**
- * Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()` or
- * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks.
- *
- * The writer can only be used inside these blocks which ensures that the model
- * can only be changed during such "sessions".
- *
- * @error writer-incorrect-use
- */
- if ( this.model._currentWriter !== this ) {
- throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' );
- }
- }
- }
- // Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
- //
- // Because attribute operation needs to have the same attribute value on the whole range, this function splits
- // the range into smaller parts.
- //
- // @private
- // @param {module:engine/model/writer~Writer} writer
- // @param {String} key Attribute key.
- // @param {*} value Attribute new value.
- // @param {module:engine/model/range~Range} range Model range on which the attribute will be set.
- function setAttributeOnRange( writer, key, value, range ) {
- const model = writer.model;
- const doc = model.document;
- // Position of the last split, the beginning of the new range.
- let lastSplitPosition = range.start;
- // Currently position in the scanning range. Because we need value after the position, it is not a current
- // position of the iterator but the previous one (we need to iterate one more time to get the value after).
- let position;
- // Value before the currently position.
- let valueBefore;
- // Value after the currently position.
- let valueAfter;
- for ( const val of range.getWalker( { shallow: true } ) ) {
- valueAfter = val.item.getAttribute( key );
- // At the first run of the iterator the position in undefined. We also do not have a valueBefore, but
- // because valueAfter may be null, valueBefore may be equal valueAfter ( undefined == null ).
- if ( position && valueBefore != valueAfter ) {
- // if valueBefore == value there is nothing to change, so we add operation only if these values are different.
- if ( valueBefore != value ) {
- addOperation();
- }
- lastSplitPosition = position;
- }
- position = val.nextPosition;
- valueBefore = valueAfter;
- }
- // Because position in the loop is not the iterator position (see let position comment), the last position in
- // the while loop will be last but one position in the range. We need to check the last position manually.
- if ( position instanceof Position && position != lastSplitPosition && valueBefore != value ) {
- addOperation();
- }
- function addOperation() {
- const range = new Range( lastSplitPosition, position );
- const version = range.root.document ? doc.version : null;
- const operation = new AttributeOperation( range, key, valueBefore, value, version );
- writer.batch.addOperation( operation );
- model.applyOperation( operation );
- }
- }
- // Sets given attribute to the given node. When attribute value is null then attribute will be removed.
- //
- // @private
- // @param {module:engine/model/writer~Writer} writer
- // @param {String} key Attribute key.
- // @param {*} value Attribute new value.
- // @param {module:engine/model/item~Item} item Model item on which the attribute will be set.
- function setAttributeOnItem( writer, key, value, item ) {
- const model = writer.model;
- const doc = model.document;
- const previousValue = item.getAttribute( key );
- let range, operation;
- if ( previousValue != value ) {
- const isRootChanged = item.root === item;
- if ( isRootChanged ) {
- // If we change attributes of root element, we have to use `RootAttributeOperation`.
- const version = item.document ? doc.version : null;
- operation = new RootAttributeOperation( item, key, previousValue, value, version );
- } else {
- range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
- const version = range.root.document ? doc.version : null;
- operation = new AttributeOperation( range, key, previousValue, value, version );
- }
- writer.batch.addOperation( operation );
- model.applyOperation( operation );
- }
- }
- // Creates and applies marker operation to {@link module:engine/model/operation/operation~Operation operation}.
- //
- // @private
- // @param {module:engine/model/writer~Writer} writer
- // @param {String} name Marker name.
- // @param {module:engine/model/range~Range} oldRange Marker range before the change.
- // @param {module:engine/model/range~Range} newRange Marker range after the change.
- // @param {Boolean} affectsData
- function applyMarkerOperation( writer, name, oldRange, newRange, affectsData ) {
- const model = writer.model;
- const doc = model.document;
- const operation = new MarkerOperation( name, oldRange, newRange, model.markers, doc.version, affectsData );
- writer.batch.addOperation( operation );
- model.applyOperation( operation );
- }
- // Creates `MoveOperation` or `DetachOperation` that removes `howMany` nodes starting from `position`.
- // The operation will be applied on given model instance and added to given operation instance.
- //
- // @private
- // @param {module:engine/model/position~Position} position Position from which nodes are removed.
- // @param {Number} howMany Number of nodes to remove.
- // @param {Batch} batch
- // @param {module:engine/model/model~Model} model Model instance on which operation will be applied.
- function applyRemoveOperation( position, howMany, batch, model ) {
- let operation;
- if ( position.root.document ) {
- const doc = model.document;
- const graveyardPosition = new Position( doc.graveyard, [ 0 ] );
- operation = new MoveOperation( position, howMany, graveyardPosition, doc.version );
- } else {
- operation = new DetachOperation( position, howMany );
- }
- batch.addOperation( operation );
- model.applyOperation( operation );
- }
- // Returns `true` if both root elements are the same element or both are documents root elements.
- //
- // Elements in the same tree can be moved (for instance you can move element form one documents root to another, or
- // within the same document fragment), but when element supposed to be moved from document fragment to the document, or
- // to another document it should be removed and inserted to avoid problems with OT. This is because features like undo or
- // collaboration may track changes on the document but ignore changes on detached fragments and should not get
- // unexpected `move` operation.
- function isSameTree( rootA, rootB ) {
- // If it is the same root this is the same tree.
- if ( rootA === rootB ) {
- return true;
- }
- // If both roots are documents root it is operation within the document what we still treat as the same tree.
- if ( rootA instanceof RootElement && rootB instanceof RootElement ) {
- return true;
- }
- return false;
- }
|