| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956 |
- /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module engine/model/batch
- */
- import AttributeDelta from './delta/attributedelta';
- import InsertDelta from './delta/insertdelta';
- import MarkerDelta from './delta/markerdelta';
- import MergeDelta from './delta/mergedelta';
- import MoveDelta from './delta/movedelta';
- import RemoveDelta from './delta/removedelta';
- import RenameDelta from './delta/renamedelta';
- import RootAttributeDelta from './delta/rootattributedelta';
- import SplitDelta from './delta/splitdelta';
- import UnwrapDelta from './delta/unwrapdelta';
- import WeakInsertDelta from './delta/weakinsertdelta';
- import WrapDelta from './delta/wrapdelta';
- 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 RemoveOperation from './operation/removeoperation';
- import RenameOperation from './operation/renameoperation';
- import RootAttributeOperation from './operation/rootattributeoperation';
- 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 toMap from '@ckeditor/ckeditor5-utils/src/tomap';
- import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
- /**
- * `Batch` instance groups document changes ({@link module:engine/model/delta/delta~Delta deltas}). All deltas grouped in a single `Batch`
- * can be reverted together, so you can think about `Batch` as of a single undo step. If you want to extend given undo step you
- * can call another method on the same `Batch` object. If you want to create a separate undo step you can create a new `Batch`.
- *
- * For example to create two separate undo steps you can call:
- *
- * doc.batch().insert( firstPosition, 'foo' );
- * doc.batch().insert( secondPosition, 'bar' );
- *
- * To create a single undo step:
- *
- * const batch = doc.batch();
- * batch.insert( firstPosition, 'foo' );
- * batch.insert( secondPosition, 'bar' );
- *
- */
- export default class Batch {
- /**
- * Creates `Batch` instance. Not recommended to use directly, use {@link module:engine/model/document~Document#batch} instead.
- *
- * @param {module:engine/model/document~Document} document Document which this Batch changes.
- * @param {'transparent'|'default'} [type='default'] Type of the batch.
- */
- constructor( document, type = 'default' ) {
- /**
- * Document which this batch changes.
- *
- * @readonly
- * @member {module:engine/model/document~Document} module:engine/model/batch~Batch#document
- */
- this.document = document;
- /**
- * Array of deltas which compose this batch.
- *
- * @readonly
- * @member {Array.<module:engine/model/delta/delta~Delta>} module:engine/model/batch~Batch#deltas
- */
- this.deltas = [];
- /**
- * Type of the batch.
- *
- * Can be one of the following values:
- * * `'default'` - all "normal" batches, most commonly used type.
- * * `'transparent'` - batch that should be ignored by other features, i.e. initial batch or collaborative editing changes.
- *
- * @readonly
- * @member {'transparent'|'default'} module:engine/model/batch~Batch#type
- */
- this.type = type;
- }
- /**
- * Returns this batch base version, which is equal to the base version of first delta in the batch.
- * If there are no deltas in the batch, it returns `null`.
- *
- * @readonly
- * @type {Number|null}
- */
- get baseVersion() {
- return this.deltas.length > 0 ? this.deltas[ 0 ].baseVersion : null;
- }
- /**
- * Adds delta to the batch instance. All modification methods (insert, remove, split, etc.) use this method
- * to add created deltas.
- *
- * @param {module:engine/model/delta/delta~Delta} delta Delta to add.
- * @return {module:engine/model/delta/delta~Delta} Added delta.
- */
- addDelta( delta ) {
- delta.batch = this;
- this.deltas.push( delta );
- return delta;
- }
- /**
- * Gets an iterable collection of operations.
- *
- * @returns {Iterable.<module:engine/model/operation/operation~Operation>}
- */
- * getOperations() {
- for ( const delta of this.deltas ) {
- yield* delta.operations;
- }
- }
- /**
- * Creates a new {@link module:engine/model/text~Text text node}.
- *
- * batch.createText( 'foo' );
- * batch.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}.
- *
- * batch.createElement( 'paragraph' );
- * batch.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 = batch.createElement( 'paragraph' );
- * batch.insert( paragraph, position );
- *
- * Instead of using position you can use parent and offset:
- *
- * const text = batch.createText( 'foo' );
- * batch.insert( text, paragraph, 5 );
- *
- * You can also use 'end' instead of the offset to insert at the end:
- *
- * const text = batch.createText( 'foo' );
- * batch.insert( text, paragraph, 'end' );
- *
- * Or insert before or after another element:
- *
- * const anotherParagraph = batch.createElement( 'paragraph' );
- * batch.insert( anotherParagraph, paragraph, 'after' );
- *
- * These parameters works the same way as {@link module:engine/model/position~Position#createAt}.
- *
- * 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/batch~Batch#move batch}.
- *
- * @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 ) {
- const position = Position.createAt( itemOrPosition, offset );
- // For text that has no parent we need to make a WeakInsert.
- const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta();
- // 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 {
- // We need to remove this item from old position first.
- this.remove( item );
- }
- }
- const insert = new InsertOperation( position, item, this.document.version );
- this.addDelta( delta );
- delta.addOperation( insert );
- this.document.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.setMarker( markerName, range );
- }
- }
- }
- /**
- * Creates and inserts text on given position. You can optionally set text attributes:
- *
- * batch.insertText( 'foo', position );
- * batch.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:
- *
- * batch.insertText( 'foo', paragraph, 5 );
- * batch.insertText( 'foo', paragraph, 'end' ); // insets at the end of the paragraph
- * batch.insertText( 'foo', image, 'after' ); // inserts after image
- *
- * These parameters works the same way as {@link module:engine/model/position~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:
- *
- * batch.insertElement( 'paragraph', position );
- * batch.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:
- *
- * batch.insertElement( 'paragraph', paragraph, 5 );
- * batch.insertElement( 'paragraph', blockquote, 'end' ); // insets at the end of the blockquote
- * batch.insertElement( 'paragraph', image, 'after' ); // inserts after image
- *
- * These parameters works the same way as {@link module:engine/model/position~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 = batch.createElement( 'paragraph' );
- * batch.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/batch~Batch#move batch}.
- *
- * @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:
- *
- * batch.appendText( 'foo', paragraph );
- * batch.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:
- *
- * batch.appendElement( 'paragraph', root );
- * batch.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 ) {
- if ( itemOrRange instanceof Range ) {
- setAttributeToRange( this, key, value, itemOrRange );
- } else {
- setAttributeToItem( 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}.
- *
- * batch.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 ) {
- if ( itemOrRange instanceof Range ) {
- setAttributeToRange( this, key, null, itemOrRange );
- } else {
- setAttributeToItem( 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 ) {
- 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.
- *
- * batch.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:
- *
- * batch.move( sourceRange, paragraph, 5 ); // moves all items in the range to the paragraph at offset 5
- * batch.move( sourceRange, blockquote, 'end' ); // moves all items in the range at the end of the blockquote
- * batch.move( sourceRange, image, 'after' ); // moves all items in the range after the image
- *
- * These parameters works the same way as {@link module:engine/model/position~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/batch~Batch#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 ) {
- if ( !( range instanceof Range ) ) {
- /**
- * Invalid range to move.
- *
- * @error batch-move-invalid-range
- */
- throw new CKEditorError( 'batch-move-invalid-range: Invalid range to move.' );
- }
- if ( !range.isFlat ) {
- /**
- * Range to move is not flat.
- *
- * @error batch-move-range-not-flat
- */
- throw new CKEditorError( 'batch-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/batch~Batch#insert insert} instead.
- *
- * @error batch-move-different-document
- */
- throw new CKEditorError( 'batch-move-different-document: Range is going to be moved between different documents.' );
- }
- const delta = new MoveDelta();
- this.addDelta( delta );
- const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, this.document.version );
- delta.addOperation( operation );
- this.document.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 ) {
- const addRemoveDelta = ( position, howMany ) => {
- const delta = new RemoveDelta();
- this.addDelta( delta );
- let operation;
- if ( position.root.document ) {
- const graveyard = this.document.graveyard;
- const gyPosition = new Position( graveyard, [ 0 ] );
- operation = new RemoveOperation( position, howMany, gyPosition, this.document.version );
- } else {
- operation = new DetachOperation( position, howMany, this.document.version );
- }
- delta.addOperation( operation );
- this.document.applyOperation( operation );
- };
- 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 ) {
- addRemoveDelta( flat.start, flat.end.offset - flat.start.offset );
- }
- } else {
- const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
- addRemoveDelta( Position.createBefore( itemOrRange ), howMany );
- }
- }
- /**
- * Merges two siblings at the given position.
- *
- * Node before and after the position have to be an element. Otherwise `batch-merge-no-element-before` or
- * `batch-merge-no-element-after` error will be thrown.
- *
- * @param {module:engine/model/position~Position} position Position of merge.
- */
- merge( position ) {
- const delta = new MergeDelta();
- this.addDelta( delta );
- const nodeBefore = position.nodeBefore;
- const nodeAfter = position.nodeAfter;
- if ( !( nodeBefore instanceof Element ) ) {
- /**
- * Node before merge position must be an element.
- *
- * @error batch-merge-no-element-before
- */
- throw new CKEditorError( 'batch-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 batch-merge-no-element-after
- */
- throw new CKEditorError( 'batch-merge-no-element-after: Node after merge position must be an element.' );
- }
- const positionAfter = Position.createFromParentAndOffset( nodeAfter, 0 );
- const positionBefore = Position.createFromParentAndOffset( nodeBefore, nodeBefore.maxOffset );
- const move = new MoveOperation(
- positionAfter,
- nodeAfter.maxOffset,
- positionBefore,
- this.document.version
- );
- move.isSticky = true;
- delta.addOperation( move );
- this.document.applyOperation( move );
- const graveyard = this.document.graveyard;
- const gyPosition = new Position( graveyard, [ 0 ] );
- const remove = new RemoveOperation( position, 1, gyPosition, this.document.version );
- delta.addOperation( remove );
- this.document.applyOperation( remove );
- }
- /**
- * Renames given element.
- *
- * @param {module:engine/model/element~Element} element The element to rename.
- * @param {String} newName New element name.
- */
- rename( element, newName ) {
- if ( !( element instanceof Element ) ) {
- /**
- * Trying to rename an object which is not an instance of Element.
- *
- * @error batch-rename-not-element-instance
- */
- throw new CKEditorError( 'batch-rename-not-element-instance: Trying to rename an object which is not an instance of Element.' );
- }
- const delta = new RenameDelta();
- this.addDelta( delta );
- const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.document.version );
- delta.addOperation( renameOperation );
- this.document.applyOperation( renameOperation );
- }
- /**
- * Splits an element at the given position.
- *
- * The element cannot be a root element, as root element cannot be split. The `batch-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.
- */
- split( position ) {
- const delta = new SplitDelta();
- this.addDelta( delta );
- const splitElement = position.parent;
- if ( !splitElement.parent ) {
- /**
- * Element with no parent can not be split.
- *
- * @error batch-split-element-no-parent
- */
- throw new CKEditorError( 'batch-split-element-no-parent: Element with no parent can not be split.' );
- }
- const copy = new Element( splitElement.name, splitElement.getAttributes() );
- const insert = new InsertOperation(
- Position.createAfter( splitElement ),
- copy,
- this.document.version
- );
- delta.addOperation( insert );
- this.document.applyOperation( insert );
- const move = new MoveOperation(
- position,
- splitElement.maxOffset - position.offset,
- Position.createFromParentAndOffset( copy, 0 ),
- this.document.version
- );
- move.isSticky = true;
- delta.addOperation( move );
- this.document.applyOperation( move );
- }
- /**
- * Wraps given range with given element or with a new element with specified name, if string has been passed.
- * **Note:** range to wrap should be a "flat range" (see {@link module:engine/model/range~Range#isFlat}). If not, 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 ) {
- if ( !range.isFlat ) {
- /**
- * Range to wrap is not flat.
- *
- * @error batch-wrap-range-not-flat
- */
- throw new CKEditorError( 'batch-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 batch-wrap-element-not-empty
- */
- throw new CKEditorError( 'batch-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 batch-wrap-element-attached
- */
- throw new CKEditorError( 'batch-wrap-element-attached: Element to wrap with is already attached to tree model.' );
- }
- const delta = new WrapDelta();
- this.addDelta( delta );
- const insert = new InsertOperation( range.end, element, this.document.version );
- delta.addOperation( insert );
- this.document.applyOperation( insert );
- const targetPosition = Position.createFromParentAndOffset( element, 0 );
- const move = new MoveOperation(
- range.start,
- range.end.offset - range.start.offset,
- targetPosition,
- this.document.version
- );
- delta.addOperation( move );
- this.document.applyOperation( move );
- }
- /**
- * 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 ) {
- if ( element.parent === null ) {
- /**
- * Trying to unwrap an element which has no parent.
- *
- * @error batch-unwrap-element-no-parent
- */
- throw new CKEditorError( 'batch-unwrap-element-no-parent: Trying to unwrap an element which has no parent.' );
- }
- const delta = new UnwrapDelta();
- this.addDelta( delta );
- const sourcePosition = Position.createFromParentAndOffset( element, 0 );
- const move = new MoveOperation(
- sourcePosition,
- element.maxOffset,
- Position.createBefore( element ),
- this.document.version
- );
- move.isSticky = true;
- delta.addOperation( move );
- this.document.applyOperation( move );
- // Computing new position because we moved some nodes before `element`.
- // If we would cache `Position.createBefore( element )` we remove wrong node.
- const graveyard = this.document.graveyard;
- const gyPosition = new Position( graveyard, [ 0 ] );
- const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.document.version );
- delta.addOperation( remove );
- this.document.applyOperation( remove );
- }
- /**
- * Adds or updates {@link module:engine/model/markercollection~Marker marker} with given name to given `range`.
- *
- * If passed name is a name of already existing marker (or {@link module:engine/model/markercollection~Marker Marker} instance
- * is passed), `range` parameter may be omitted. In this case marker will not be updated in
- * {@link module:engine/model/document~Document#markers document marker collection}. However the marker will be added to
- * the document history. This may be important for other features, like undo. From document history point of view, it will
- * look like the marker was created and added to the document at the moment when it is set using this method.
- *
- * This is useful if the marker is created before it can be added to document history (e.g. a feature creating the marker
- * is waiting for additional data, etc.). In this case, the marker may be first created directly through
- * {@link module:engine/model/markercollection~MarkerCollection MarkerCollection API} and only later added using `Batch` API.
- *
- * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to add or update.
- * @param {module:engine/model/range~Range} [newRange] Marker range.
- */
- setMarker( markerOrName, newRange ) {
- const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
- const currentMarker = this.document.markers.get( name );
- if ( !newRange && !currentMarker ) {
- /**
- * Range parameter is required when adding a new marker.
- *
- * @error batch-setMarker-no-range
- */
- throw new CKEditorError( 'batch-setMarker-no-range: Range parameter is required when adding a new marker.' );
- }
- const currentRange = currentMarker ? currentMarker.getRange() : null;
- if ( !newRange ) {
- // If `newRange` is not given, treat this as synchronizing existing marker.
- // Create `MarkerOperation` with `oldRange` set to `null`, so reverse operation will remove the marker.
- addMarkerOperation( this, name, null, currentRange );
- } else {
- // Just change marker range.
- addMarkerOperation( this, name, currentRange, newRange );
- }
- }
- /**
- * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name.
- *
- * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove.
- */
- removeMarker( markerOrName ) {
- const name = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
- if ( !this.document.markers.has( name ) ) {
- /**
- * Trying to remove marker which does not exist.
- *
- * @error batch-removeMarker-no-marker
- */
- throw new CKEditorError( 'batch-removeMarker-no-marker: Trying to remove marker which does not exist.' );
- }
- const oldRange = this.document.markers.get( name ).getRange();
- addMarkerOperation( this, name, oldRange, null );
- }
- }
- // 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/batch~Batch} batch
- // @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 setAttributeToRange( batch, key, value, range ) {
- const delta = new AttributeDelta();
- const doc = batch.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 ) {
- 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() {
- // Add delta to the batch only if there is at least operation in the delta. Add delta only once.
- if ( delta.operations.length === 0 ) {
- batch.addDelta( delta );
- }
- const range = new Range( lastSplitPosition, position );
- const operation = new AttributeOperation( range, key, valueBefore, value, doc.version );
- delta.addOperation( operation );
- doc.applyOperation( operation );
- }
- }
- // Sets given attribute to the given node. When attribute value is null then attribute will be removed.
- //
- // @private
- // @param {module:engine/model/batch~Batch} batch
- // @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 setAttributeToItem( batch, key, value, item ) {
- const doc = batch.document;
- const previousValue = item.getAttribute( key );
- let range, operation;
- if ( previousValue != value ) {
- const delta = item.root === item ? new RootAttributeDelta() : new AttributeDelta();
- batch.addDelta( delta );
- if ( item.root === item ) {
- // If we change attributes of root element, we have to use `RootAttributeOperation`.
- operation = new RootAttributeOperation( item, key, previousValue, value, doc.version );
- } else {
- if ( item.is( 'element' ) ) {
- // If we change the attribute of the element, we do not want to change attributes of its children, so
- // the end of the range cannot be after the closing tag, it should be inside that element, before any of
- // it's children, so the range will contain only the opening tag.
- range = new Range( Position.createBefore( item ), Position.createFromParentAndOffset( item, 0 ) );
- } else {
- // If `item` is text proxy, we create a range from the beginning to the end of that text proxy, to change
- // all characters represented by it.
- range = new Range( Position.createBefore( item ), Position.createAfter( item ) );
- }
- operation = new AttributeOperation( range, key, previousValue, value, doc.version );
- }
- delta.addOperation( operation );
- doc.applyOperation( operation );
- }
- }
- // Creates and adds marker operation to {@link module:engine/model/delta/delta~Delta delta}.
- //
- // @private
- // @param {module:engine/model/batch~Batch} batch
- // @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.
- function addMarkerOperation( batch, name, oldRange, newRange ) {
- const doc = batch.document;
- const delta = new MarkerDelta();
- const operation = new MarkerOperation( name, oldRange, newRange, doc.markers, doc.version );
- batch.addDelta( delta );
- delta.addOperation( operation );
- doc.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 ( rootA === rootB ) {
- return true;
- }
- return rootA instanceof RootElement && rootB instanceof RootElement;
- }
|