From f74369f4b595932dc652d591230675180fb8a51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 17 Nov 2017 11:12:59 +0100 Subject: [PATCH 01/44] Removed Batch#register moethod and moved all batch methods to the Batch class. --- src/model/batch.js | 682 +++++++++++++++- src/model/delta/attributedelta.js | 131 --- src/model/delta/basic-deltas.js | 1 - src/model/delta/insertdelta.js | 50 -- src/model/delta/markerdelta.js | 85 -- src/model/delta/mergedelta.js | 65 -- src/model/delta/movedelta.js | 41 - src/model/delta/removedelta.js | 38 - src/model/delta/renamedelta.js | 37 - src/model/delta/splitdelta.js | 55 -- src/model/delta/unwrapdelta.js | 51 -- src/model/delta/weakinsertdelta.js | 40 - src/model/delta/wrapdelta.js | 65 -- tests/model/batch.js | 1115 +++++++++++++++++++++++++- tests/model/delta/attributedelta.js | 398 --------- tests/model/delta/insertdelta.js | 88 -- tests/model/delta/markerdelta.js | 111 --- tests/model/delta/mergedelta.js | 62 -- tests/model/delta/movedelta.js | 67 -- tests/model/delta/removedelta.js | 71 -- tests/model/delta/renamedelta.js | 46 -- tests/model/delta/splitdelta.js | 78 -- tests/model/delta/unwrapdelta.js | 50 -- tests/model/delta/weakinsertdelta.js | 50 -- tests/model/delta/wrapdelta.js | 83 -- 25 files changed, 1713 insertions(+), 1847 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index 70a04b1c4..a871c5c2c 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -7,6 +7,33 @@ * @module engine/model/batch */ +import { default as AttributeDelta, RootAttributeDelta } 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 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 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 Element from './element'; +import Position from './position'; +import Range from './range.js'; + +import { normalizeNodes } from './writer'; + import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** @@ -101,55 +128,626 @@ export default class Batch { yield* delta.operations; } } + + /** + * Inserts a node or nodes at the given position. + * + * When inserted element is a {@link module:engine/model/documentfragment~DocumentFragment} and has markers its markers will + * be set to {@link module:engine/model/document~Document#markers}. + * + * @chainable + * @param {module:engine/model/position~Position} position Position of insertion. + * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. + */ + insert( position, nodes ) { + const normalizedNodes = normalizeNodes( nodes ); + + // If nothing is inserted do not create delta and operation. + if ( normalizedNodes.length === 0 ) { + return this; + } + + const delta = new InsertDelta(); + const insert = new InsertOperation( position, normalizedNodes, 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 ( nodes instanceof DocumentFragment ) { + for ( const [ markerName, markerRange ] of nodes.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 ); + } + } + + return this; + } + + /** + * Inserts a node or nodes at given position. {@link module:engine/model/batch~Batch#weakInsert weakInsert} is commonly used for actions + * like typing or plain-text paste (without formatting). There are two differences between + * {@link module:engine/model/batch~Batch#insert insert} and {@link module:engine/model/batch~Batch#weakInsert weakInsert}: + * + * * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of + * {@link module:engine/model/document~Document#selection document selection}. + * * If {@link module:engine/model/operation/insertoperation~InsertOperation insert operation} position is inside a range changed by + * {@link module:engine/model/operation/attributeoperation~AttributeOperation attribute operation}, + * the attribute operation is split into two operations. + * Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that + * {@link module:engine/model/operation/attributeoperation~AttributeOperation AttributeOperation} does not "break" and also + * applies attributes for inserted nodes. This behavior has to be reflected during + * {@link module:engine/model/delta/transform~transform delta transformation}. + * + * @chainable + * @param {module:engine/model/position~Position} position Position of insertion. + * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. + */ + weakInsert( position, nodes ) { + const delta = new WeakInsertDelta(); + this.addDelta( delta ); + + nodes = normalizeNodes( nodes ); + + for ( const node of nodes ) { + node.setAttributesTo( this.document.selection.getAttributes() ); + } + + const operation = new InsertOperation( position, nodes, this.document.version ); + delta.addOperation( operation ); + this.document.applyOperation( operation ); + + return this; + } + + /** + * 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}. + * + * @chainable + * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange + * Model item or range on which the attribute will be set. + * @param {String} key Attribute key. + * @param {*} value Attribute new value. + */ + setAttribute( itemOrRange, key, value ) { + if ( itemOrRange instanceof Range ) { + setAttributeToRange( this, key, value, itemOrRange ); + } else { + setAttributeToItem( this, key, value, itemOrRange ); + } + + return this; + } + + /** + * 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}. + * + * @chainable + * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange + * Model item or range from which the attribute will be removed. + * @method module:engine/model/batch~Batch#removeAttribute + * @param {String} key Attribute key. + */ + removeAttribute( itemOrRange, key ) { + if ( itemOrRange instanceof Range ) { + setAttributeToRange( this, key, null, itemOrRange ); + } else { + setAttributeToItem( this, key, null, itemOrRange ); + } + + return this; + } + + /** + * 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. + * + * @chainable + * @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 ); + + return this; + } + + /** + * Moves given {@link module:engine/model/item~Item model item} or given range to target position. + * + * @chainable + * @method module:engine/model/batch~Batch#move + * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range of nodes to move. + * @param {module:engine/model/position~Position} targetPosition Position where moved nodes will be inserted. + */ + move( itemOrRange, targetPosition ) { + const delta = new MoveDelta(); + this.addDelta( delta ); + + const addOperation = ( sourcePosition, howMany, targetPosition ) => { + const operation = new MoveOperation( sourcePosition, howMany, targetPosition, this.document.version ); + delta.addOperation( operation ); + this.document.applyOperation( operation ); + }; + + if ( itemOrRange instanceof Range ) { + if ( !itemOrRange.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.' ); + } + + addOperation( itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition ); + } else { + addOperation( Position.createBefore( itemOrRange ), 1, targetPosition ); + } + + return this; + } + + /** + * Removes given {@link module:engine/model/item~Item model item} or given range. + * + * @chainable + * @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 ); + + const graveyard = this.document.graveyard; + const gyPosition = new Position( graveyard, [ 0 ] ); + + const operation = new RemoveOperation( position, howMany, gyPosition, 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 { + addRemoveDelta( Position.createBefore( itemOrRange ), 1 ); + } + + return this; + } + + /** + * Renames given element. + * + * @chainable + * @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 ); + + return this; + } + + /** + * Splits an element at the given position. + * + * The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if + * you try to split the root element. + * + * @chainable + * @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 ) { + /** + * Root element can not be split. + * + * @error batch-split-root + */ + throw new CKEditorError( 'batch-split-root: Root element 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 ); + + return this; + } + + /** + * 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. + * + * @chainable + * @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 ); + + return this; + } + + /** + * 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. + * + * @chainable + * @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 ); + + return this; + } + + /** + * 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. + * + * @chainable + * @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 ); + } + + return this; + } + + /** + * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name. + * + * @chainable + * @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 ); + + return this; + } } /** - * Function to register batch methods. To make code scalable `Batch` do not have modification - * methods built in. They can be registered using this method. - * - * This method checks if there is no naming collision and throws `batch-register-taken` if the method name - * is already taken. - * - * Besides that no magic happens here, the method is added to the `Batch` class prototype. - * - * For example: - * - * Batch.register( 'insert', function( position, nodes ) { - * // You can use a class inheriting from `Delta` if that class should handle OT in a special way. - * const delta = new Delta(); - * - * // Add delta to the Batch instance. It is important to add a delta to the batch before applying any operation. - * this.addDelta( delta ); - * - * // Create operations which should be components of this delta. - * const operation = new InsertOperation( position, nodes, this.document.version ); - * - * // Add operation to the delta. It is important to add operation before applying it. - * delta.addOperation( operation ); + * Sets given attribute to each node in given range. When attribute value is null then attribute will be removed. * - * // Remember to apply every operation, no magic, you need to do it manually. - * this.document.applyOperation( operation ); + * Because attribute operation needs to have the same attribute value on the whole range, this function splits + * the range into smaller parts. * - * // Make this method chainable. - * return this; - * } ); + * @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. * - * @method module:engine/model/batch~Batch.register - * @param {String} name Method name. - * @param {Function} creator Method body. + * @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. */ -export function register( name, creator ) { - if ( Batch.prototype[ name ] ) { - /** - * This batch method name is already taken. - * - * @error batch-register-taken - * @param {String} name - */ - throw new CKEditorError( - 'model-batch-register-taken: This batch method name is already taken.', - { name } ); +function setAttributeToItem( batch, key, value, item ) { + const doc = batch.document; + const previousValue = item.getAttribute( key ); + let range, operation; + + const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta(); + + if ( previousValue != value ) { + batch.addDelta( delta ); + + if ( item.is( 'rootElement' ) ) { + // 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.prototype[ name ] = creator; + batch.addDelta( delta ); + delta.addOperation( operation ); + doc.applyOperation( operation ); } diff --git a/src/model/delta/attributedelta.js b/src/model/delta/attributedelta.js index cba9b12c2..fa27e2256 100644 --- a/src/model/delta/attributedelta.js +++ b/src/model/delta/attributedelta.js @@ -9,11 +9,7 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; -import { register } from '../batch'; -import AttributeOperation from '../operation/attributeoperation'; -import RootAttributeOperation from '../operation/rootattributeoperation'; import NoOperation from '../operation/nooperation'; -import Position from '../position'; import Range from '../range'; /** @@ -129,132 +125,5 @@ export class RootAttributeDelta extends Delta { } } -/** - * 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}. - * - * @chainable - * @method module:engine/model/batch~Batch#setAttribute - * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange - * Model item or range on which the attribute will be set. - * @param {String} key Attribute key. - * @param {*} value Attribute new value. - */ -register( 'setAttribute', function( itemOrRange, key, value ) { - attribute( this, key, value, itemOrRange ); - - return this; -} ); - -/** - * 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}. - * - * @chainable - * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange - * Model item or range from which the attribute will be removed. - * @method module:engine/model/batch~Batch#removeAttribute - * @param {String} key Attribute key. - */ -register( 'removeAttribute', function( itemOrRange, key ) { - attribute( this, key, null, itemOrRange ); - - return this; -} ); - -function attribute( batch, key, value, itemOrRange ) { - if ( itemOrRange instanceof Range ) { - changeRange( batch, batch.document, key, value, itemOrRange ); - } else { - changeItem( batch, batch.document, key, value, itemOrRange ); - } -} - -function changeItem( batch, doc, key, value, item ) { - const previousValue = item.getAttribute( key ); - let range, operation; - - const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta(); - - if ( previousValue != value ) { - batch.addDelta( delta ); - - if ( item.is( 'rootElement' ) ) { - // 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 ); - } -} - -// Because attribute operation needs to have the same attribute value on the whole range, this function splits the range -// into smaller parts. -function changeRange( batch, doc, attributeKey, attributeValue, range ) { - const delta = new AttributeDelta(); - - // 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. - attributeValueBefore, - // Value after the currently position. - attributeValueAfter; - - for ( const value of range ) { - attributeValueAfter = value.item.getAttribute( attributeKey ); - - // At the first run of the iterator the position in undefined. We also do not have a attributeValueBefore, but - // because attributeValueAfter may be null, attributeValueBefore may be equal attributeValueAfter ( undefined == null ). - if ( position && attributeValueBefore != attributeValueAfter ) { - // if attributeValueBefore == attributeValue there is nothing to change, so we add operation only if these values are different. - if ( attributeValueBefore != attributeValue ) { - addOperation(); - } - - lastSplitPosition = position; - } - - position = value.nextPosition; - attributeValueBefore = attributeValueAfter; - } - - // 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 && attributeValueBefore != attributeValue ) { - 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, attributeKey, attributeValueBefore, attributeValue, doc.version ); - - delta.addOperation( operation ); - doc.applyOperation( operation ); - } -} - DeltaFactory.register( AttributeDelta ); DeltaFactory.register( RootAttributeDelta ); diff --git a/src/model/delta/basic-deltas.js b/src/model/delta/basic-deltas.js index 3d4a8b7e7..5161ccd03 100644 --- a/src/model/delta/basic-deltas.js +++ b/src/model/delta/basic-deltas.js @@ -13,7 +13,6 @@ // Import default suite of deltas so a feature have to include only Batch class file. import './attributedelta'; -import './insertdelta'; import './mergedelta'; import './movedelta'; import './removedelta'; diff --git a/src/model/delta/insertdelta.js b/src/model/delta/insertdelta.js index 641b68a0f..370dd1e60 100644 --- a/src/model/delta/insertdelta.js +++ b/src/model/delta/insertdelta.js @@ -10,13 +10,6 @@ import Delta from './delta'; import RemoveDelta from './removedelta'; import DeltaFactory from './deltafactory'; -import InsertOperation from '../operation/insertoperation'; -import { register } from '../batch'; -import { normalizeNodes } from './../writer'; - -import DocumentFragment from '../documentfragment'; -import Range from '../../model/range.js'; -import Position from '../../model/position.js'; /** * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert Batch#insert} method @@ -78,47 +71,4 @@ export default class InsertDelta extends Delta { } } -/** - * Inserts a node or nodes at the given position. - * - * When inserted element is a {@link module:engine/model/documentfragment~DocumentFragment} and has markers its markers will - * be set to {@link module:engine/model/document~Document#markers}. - * - * @chainable - * @method module:engine/model/batch~Batch#insert - * @param {module:engine/model/position~Position} position Position of insertion. - * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. - */ -register( 'insert', function( position, nodes ) { - const normalizedNodes = normalizeNodes( nodes ); - - // If nothing is inserted do not create delta and operation. - if ( normalizedNodes.length === 0 ) { - return this; - } - - const delta = new InsertDelta(); - const insert = new InsertOperation( position, normalizedNodes, 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 ( nodes instanceof DocumentFragment ) { - for ( const [ markerName, markerRange ] of nodes.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 ); - } - } - - return this; -} ); - DeltaFactory.register( InsertDelta ); diff --git a/src/model/delta/markerdelta.js b/src/model/delta/markerdelta.js index f4f09bf1c..32bc8a25d 100644 --- a/src/model/delta/markerdelta.js +++ b/src/model/delta/markerdelta.js @@ -9,9 +9,6 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; -import { register } from '../batch'; -import MarkerOperation from '../operation/markeroperation'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#setMarker Batch#setMarker} @@ -46,86 +43,4 @@ export default class MarkerDelta extends Delta { } } -/** - * 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. - * - * @chainable - * @method module:engine/model/batch~Batch#setMarker - * @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. - */ -register( 'setMarker', function( 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. - addOperation( this, name, null, currentRange ); - } else { - // Just change marker range. - addOperation( this, name, currentRange, newRange ); - } - - return this; -} ); - -/** - * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name. - * - * @chainable - * @method module:engine/model/batch~Batch#removeMarker - * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove. - */ -register( 'removeMarker', function( 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(); - - addOperation( this, name, oldRange, null ); - - return this; -} ); - -function addOperation( 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 ); -} - DeltaFactory.register( MarkerDelta ); diff --git a/src/model/delta/mergedelta.js b/src/model/delta/mergedelta.js index 0bc69da2c..5cd83b5eb 100644 --- a/src/model/delta/mergedelta.js +++ b/src/model/delta/mergedelta.js @@ -10,12 +10,6 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; import SplitDelta from './splitdelta'; -import { register } from '../batch'; -import Position from '../position'; -import Element from '../element'; -import RemoveOperation from '../operation/removeoperation'; -import MoveOperation from '../operation/moveoperation'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method @@ -70,63 +64,4 @@ export default class MergeDelta extends Delta { } } -/** - * 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. - * - * @chainable - * @method module:engine/model/batch~Batch#merge - * @param {module:engine/model/position~Position} position Position of merge. - */ -register( 'merge', function( 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 ); - - return this; -} ); - DeltaFactory.register( MergeDelta ); diff --git a/src/model/delta/movedelta.js b/src/model/delta/movedelta.js index f35b1af05..4a34ce2aa 100644 --- a/src/model/delta/movedelta.js +++ b/src/model/delta/movedelta.js @@ -9,11 +9,6 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; -import { register } from '../batch'; -import MoveOperation from '../operation/moveoperation'; -import Position from '../position'; -import Range from '../range'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#move} method @@ -86,40 +81,4 @@ export default class MoveDelta extends Delta { } } -function addMoveOperation( batch, delta, sourcePosition, howMany, targetPosition ) { - const operation = new MoveOperation( sourcePosition, howMany, targetPosition, batch.document.version ); - delta.addOperation( operation ); - batch.document.applyOperation( operation ); -} - -/** - * Moves given {@link module:engine/model/item~Item model item} or given range to target position. - * - * @chainable - * @method module:engine/model/batch~Batch#move - * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range of nodes to move. - * @param {module:engine/model/position~Position} targetPosition Position where moved nodes will be inserted. - */ -register( 'move', function( itemOrRange, targetPosition ) { - const delta = new MoveDelta(); - this.addDelta( delta ); - - if ( itemOrRange instanceof Range ) { - if ( !itemOrRange.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.' ); - } - - addMoveOperation( this, delta, itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition ); - } else { - addMoveOperation( this, delta, Position.createBefore( itemOrRange ), 1, targetPosition ); - } - - return this; -} ); - DeltaFactory.register( MoveDelta ); diff --git a/src/model/delta/removedelta.js b/src/model/delta/removedelta.js index 3db9e34aa..8756f717f 100644 --- a/src/model/delta/removedelta.js +++ b/src/model/delta/removedelta.js @@ -8,11 +8,7 @@ */ import MoveDelta from './movedelta'; -import { register } from '../batch'; import DeltaFactory from './deltafactory'; -import RemoveOperation from '../operation/removeoperation'; -import Position from '../position'; -import Range from '../range'; /** * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#remove} method @@ -29,38 +25,4 @@ export default class RemoveDelta extends MoveDelta { } } -function addRemoveDelta( batch, position, howMany ) { - const delta = new RemoveDelta(); - batch.addDelta( delta ); - - const graveyard = batch.document.graveyard; - const gyPosition = new Position( graveyard, [ 0 ] ); - - const operation = new RemoveOperation( position, howMany, gyPosition, batch.document.version ); - delta.addOperation( operation ); - batch.document.applyOperation( operation ); -} - -/** - * Removes given {@link module:engine/model/item~Item model item} or given range. - * - * @chainable - * @method module:engine/model/batch~Batch#remove - * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove. - */ -register( 'remove', function( itemOrRange ) { - 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( this, flat.start, flat.end.offset - flat.start.offset ); - } - } else { - addRemoveDelta( this, Position.createBefore( itemOrRange ), 1 ); - } - - return this; -} ); - DeltaFactory.register( RemoveDelta ); diff --git a/src/model/delta/renamedelta.js b/src/model/delta/renamedelta.js index 0dcd32ed5..d39780726 100644 --- a/src/model/delta/renamedelta.js +++ b/src/model/delta/renamedelta.js @@ -9,11 +9,6 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; -import { register } from '../batch'; -import RenameOperation from '../operation/renameoperation'; -import Element from '../element'; -import Position from '../position'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#rename Batch#rename} method @@ -44,36 +39,4 @@ export default class RenameDelta extends Delta { } } -function apply( batch, delta, operation ) { - delta.addOperation( operation ); - batch.document.applyOperation( operation ); -} - -/** - * Renames given element. - * - * @chainable - * @method module:engine/model/batch~Batch#rename - * @param {module:engine/model/element~Element} element The element to rename. - * @param {String} newName New element name. - */ -register( 'rename', function( 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 ); - apply( this, delta, renameOperation ); - - return this; -} ); - DeltaFactory.register( RenameDelta ); diff --git a/src/model/delta/splitdelta.js b/src/model/delta/splitdelta.js index b62ddd9da..fdc042cd0 100644 --- a/src/model/delta/splitdelta.js +++ b/src/model/delta/splitdelta.js @@ -9,12 +9,7 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; -import { register } from '../batch'; -import Position from '../position'; -import Element from '../element'; -import InsertOperation from '../operation/insertoperation'; import MoveOperation from '../operation/moveoperation'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import MergeDelta from '../delta/mergedelta'; /** @@ -85,54 +80,4 @@ export default class SplitDelta extends Delta { } } -/** - * Splits an element at the given position. - * - * The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if - * you try to split the root element. - * - * @chainable - * @method module:engine/model/batch~Batch#split - * @param {module:engine/model/position~Position} position Position of split. - */ -register( 'split', function( position ) { - const delta = new SplitDelta(); - this.addDelta( delta ); - - const splitElement = position.parent; - - if ( !splitElement.parent ) { - /** - * Root element can not be split. - * - * @error batch-split-root - */ - throw new CKEditorError( 'batch-split-root: Root element 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 ); - - return this; -} ); - DeltaFactory.register( SplitDelta ); diff --git a/src/model/delta/unwrapdelta.js b/src/model/delta/unwrapdelta.js index d37b87db1..23f7aadf9 100644 --- a/src/model/delta/unwrapdelta.js +++ b/src/model/delta/unwrapdelta.js @@ -10,11 +10,6 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; import WrapDelta from './wrapdelta'; -import { register } from '../batch'; -import Position from '../position'; -import RemoveOperation from '../operation/removeoperation'; -import MoveOperation from '../operation/moveoperation'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method @@ -64,50 +59,4 @@ export default class UnwrapDelta extends Delta { } } -/** - * 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. - * - * @chainable - * @method module:engine/model/batch~Batch#unwrap - * @param {module:engine/model/element~Element} position Element to unwrap. - */ -register( 'unwrap', function( 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 ); - - return this; -} ); - DeltaFactory.register( UnwrapDelta ); diff --git a/src/model/delta/weakinsertdelta.js b/src/model/delta/weakinsertdelta.js index 5fe32dab8..8c63b5187 100644 --- a/src/model/delta/weakinsertdelta.js +++ b/src/model/delta/weakinsertdelta.js @@ -8,10 +8,7 @@ */ import InsertDelta from './insertdelta'; -import { register } from '../batch'; import DeltaFactory from './deltafactory'; -import InsertOperation from '../operation/insertoperation'; -import { normalizeNodes } from './../writer'; /** * To provide specific OT behavior and better collisions solving, the {@link module:engine/model/batch~Batch#insert} method @@ -28,41 +25,4 @@ export default class WeakInsertDelta extends InsertDelta { } } -/** - * Inserts a node or nodes at given position. {@link module:engine/model/batch~Batch#weakInsert weakInsert} is commonly used for actions - * like typing or plain-text paste (without formatting). There are two differences between - * {@link module:engine/model/batch~Batch#insert insert} and {@link module:engine/model/batch~Batch#weakInsert weakInsert}: - * - * * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of - * {@link module:engine/model/document~Document#selection document selection}. - * * If {@link module:engine/model/operation/insertoperation~InsertOperation insert operation} position is inside a range changed by - * {@link module:engine/model/operation/attributeoperation~AttributeOperation attribute operation}, - * the attribute operation is split into two operations. - * Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that - * {@link module:engine/model/operation/attributeoperation~AttributeOperation AttributeOperation} does not "break" and also - * applies attributes for inserted nodes. This behavior has to be reflected during - * {@link module:engine/model/delta/transform~transform delta transformation}. - * - * @chainable - * @method module:engine/model/batch~Batch#weakInsert - * @param {module:engine/model/position~Position} position Position of insertion. - * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. - */ -register( 'weakInsert', function( position, nodes ) { - const delta = new WeakInsertDelta(); - this.addDelta( delta ); - - nodes = normalizeNodes( nodes ); - - for ( const node of nodes ) { - node.setAttributesTo( this.document.selection.getAttributes() ); - } - - const operation = new InsertOperation( position, nodes, this.document.version ); - delta.addOperation( operation ); - this.document.applyOperation( operation ); - - return this; -} ); - DeltaFactory.register( WeakInsertDelta ); diff --git a/src/model/delta/wrapdelta.js b/src/model/delta/wrapdelta.js index 5f0dc0699..391a9a4f8 100644 --- a/src/model/delta/wrapdelta.js +++ b/src/model/delta/wrapdelta.js @@ -10,13 +10,7 @@ import Delta from './delta'; import DeltaFactory from './deltafactory'; import UnwrapDelta from './unwrapdelta'; -import { register } from '../batch'; -import Position from '../position'; import Range from '../range'; -import Element from '../element'; -import InsertOperation from '../operation/insertoperation'; -import MoveOperation from '../operation/moveoperation'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * To provide specific OT behavior and better collisions solving, {@link module:engine/model/batch~Batch#merge} method @@ -91,63 +85,4 @@ export default class WrapDelta extends Delta { } } -/** - * 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. - * - * @chainable - * @method module:engine/model/batch~Batch#wrap - * @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. - */ -register( 'wrap', function( 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 ); - - return this; -} ); - DeltaFactory.register( WrapDelta ); diff --git a/tests/model/batch.js b/tests/model/batch.js index 5cbcae0da..207d7428a 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -3,22 +3,27 @@ * For licensing, see LICENSE.md. */ -import deltas from '../../src/model/delta/basic-deltas'; // eslint-disable-line no-unused-vars - -import Document from '../../src/model/document'; -import { default as Batch, register } from '../../src/model/batch'; +import Batch from '../../src/model/batch'; import Delta from '../../src/model/delta/delta'; + import Operation from '../../src/model/operation/operation'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import InsertOperation from '../../src/model/operation/insertoperation'; +import MarkerOperation from '../../src/model/operation/markeroperation'; -describe( 'Batch', () => { - it( 'should have registered basic methods', () => { - const batch = new Batch( new Document() ); +import Document from '../../src/model/document'; +import DocumentFragment from '../../src/model/documentfragment'; +import Element from '../../src/model/element'; +import Text from '../../src/model/text'; +import Position from '../../src/model/position'; +import Range from '../../src/model/range'; - expect( batch.setAttribute ).to.be.a( 'function' ); - expect( batch.removeAttribute ).to.be.a( 'function' ); - } ); +import count from '@ckeditor/ckeditor5-utils/src/count'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; + +import { stringify } from '../../src/dev-utils/model'; +import { getNodesAndText } from '../../tests/model/_utils/utils'; +describe( 'Batch', () => { describe( 'type', () => { it( 'should default to "default"', () => { const batch = new Batch( new Document() ); @@ -33,33 +38,25 @@ describe( 'Batch', () => { } ); } ); - describe( 'register', () => { - afterEach( () => { - delete Batch.prototype.foo; - } ); - - it( 'should register function to the batch prototype', () => { - const spy = sinon.spy(); - - register( 'foo', spy ); - + describe( 'baseVersion', () => { + it( 'should return base version of first delta from the batch', () => { const batch = new Batch( new Document() ); + const delta = new Delta(); + const operation = new Operation( 2 ); + delta.addOperation( operation ); + batch.addDelta( delta ); - batch.foo(); - - expect( spy.calledOnce ).to.be.true; + expect( batch.baseVersion ).to.equal( 2 ); } ); - it( 'should throw if one try to register the same batch twice', () => { - register( 'foo', () => {} ); + it( 'should return null if there are no deltas in batch', () => { + const batch = new Batch( new Document() ); - expect( () => { - register( 'foo', () => {} ); - } ).to.throw( CKEditorError, /^model-batch-register-taken/ ); + expect( batch.baseVersion ).to.be.null; } ); } ); - describe( 'addDelta', () => { + describe( 'addDelta()', () => { it( 'should add delta to the batch', () => { const batch = new Batch( new Document() ); const deltaA = new Delta(); @@ -73,7 +70,7 @@ describe( 'Batch', () => { } ); } ); - describe( 'getOperations', () => { + describe( 'getOperations()', () => { it( 'should return collection of operations from all deltas', () => { const doc = new Document(); const batch = new Batch( doc ); @@ -96,21 +93,1055 @@ describe( 'Batch', () => { } ); } ); - describe( 'baseVersion', () => { - it( 'should return base version of first delta from the batch', () => { - const batch = new Batch( new Document() ); - const delta = new Delta(); - const operation = new Operation( 2 ); - delta.addOperation( operation ); - batch.addDelta( delta ); + describe( 'insert', () => { + let doc, root, batch, p, ul, chain; - expect( batch.baseVersion ).to.equal( 2 ); + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + root.insertChildren( 0, new Text( 'abc' ) ); + + batch = doc.batch(); + + p = new Element( 'p' ); + ul = new Element( 'ul' ); + + chain = batch.insert( new Position( root, [ 2 ] ), [ p, ul ] ); } ); - it( 'should return null if there are no deltas in batch', () => { - const batch = new Batch( new Document() ); + it( 'should insert given nodes at given position', () => { + expect( root.childCount ).to.equal( 4 ); + expect( root.maxOffset ).to.equal( 5 ); + expect( root.getChild( 1 ) ).to.equal( p ); + expect( root.getChild( 2 ) ).to.equal( ul ); + } ); - expect( batch.baseVersion ).to.be.null; + it( 'should be chainable', () => { + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.insert( new Position( root, [ 2 ] ), [ p, ul ] ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + + it( 'should transfer markers from given DocumentFragment', () => { + const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] ); + const marker = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 5 ] ) ); + + documentFragment.markers.set( 'marker', marker ); + + batch.insert( new Position( root, [ 3, 0 ] ), documentFragment ); + + expect( Array.from( doc.markers ).length ).to.equal( 1 ); + expect( stringify( root, doc.markers.get( 'marker' ).getRange() ) ).to.equal( 'ab

c' ); + } ); + + it( 'should set each marker as separate operation', () => { + sinon.spy( doc, 'applyOperation' ); + + const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] ); + const marker1 = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 2 ] ) ); + const marker2 = new Range( new Position( documentFragment, [ 0, 5 ] ), new Position( documentFragment, [ 0, 6 ] ) ); + + documentFragment.markers.set( 'marker1', marker1 ); + documentFragment.markers.set( 'marker2', marker2 ); + + batch.insert( new Position( root, [ 3, 0 ] ), documentFragment ); + + expect( doc.applyOperation.calledThrice ); + expect( doc.applyOperation.firstCall.calledWith( sinon.match( operation => operation instanceof InsertOperation ) ) ); + expect( doc.applyOperation.secondCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); + expect( doc.applyOperation.thirdCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); + } ); + + it( 'should not create a delta and an operation if no nodes were inserted', () => { + sinon.spy( doc, 'applyOperation' ); + + batch = doc.batch(); + + batch.insert( new Position( root, [ 0 ] ), [] ); + + expect( batch.deltas.length ).to.equal( 0 ); + expect( doc.applyOperation.called ).to.be.false; + } ); + } ); + + describe( 'weakInsert()', () => { + let doc, root, batch, chain, attrs; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + root.insertChildren( 0, new Text( 'abc' ) ); + + batch = doc.batch(); + + attrs = [ [ 'bold', true ], [ 'foo', 'bar' ] ]; + + doc.selection.setAttributesTo( attrs ); + + chain = batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' ); + } ); + + it( 'should insert given nodes at given position', () => { + expect( root.maxOffset ).to.equal( 6 ); + expect( root.getChild( 0 ).data ).to.equal( 'ab' ); + expect( root.getChild( 1 ).data ).to.equal( 'xyz' ); + expect( root.getChild( 2 ).data ).to.equal( 'c' ); + } ); + + it( 'should set inserted nodes attributes to same as current selection attributes', () => { + expect( Array.from( root.getChild( 1 ).getAttributes() ) ).to.deep.equal( attrs ); + } ); + + it( 'should be chainable', () => { + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'setAttribute() / removeAttribute()', () => { + let batch, doc, root; + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + batch = doc.batch(); + } ); + + function getOperationsCount() { + let totalNumber = 0; + + for ( const delta of batch.deltas ) { + totalNumber += count( delta.operations ); + } + + return totalNumber; + } + + describe( 'change attribute on node', () => { + let node, text; + + beforeEach( () => { + node = new Element( 'p', { a: 1 } ); + text = new Text( 'c', { a: 1 } ); + + root.insertChildren( 0, [ node, text ] ); + } ); + + describe( 'setAttribute', () => { + it( 'should create the attribute on element', () => { + batch.setAttribute( node, 'b', 2 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( node.getAttribute( 'b' ) ).to.equal( 2 ); + } ); + + it( 'should change the attribute of element', () => { + batch.setAttribute( node, 'a', 2 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( node.getAttribute( 'a' ) ).to.equal( 2 ); + } ); + + it( 'should create the attribute on text node', () => { + batch.setAttribute( text, 'b', 2 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 ); + } ); + + it( 'should change the attribute of text node', () => { + batch.setAttribute( text, 'a', 2 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 ); + } ); + + it( 'should do nothing if the attribute value is the same', () => { + batch.setAttribute( node, 'a', 1 ); + expect( getOperationsCount() ).to.equal( 0 ); + expect( node.getAttribute( 'a' ) ).to.equal( 1 ); + } ); + + it( 'should be chainable', () => { + const chain = batch.setAttribute( node, 'b', 2 ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.setAttribute( node, 'b', 2 ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'removeAttribute', () => { + it( 'should remove the attribute from element', () => { + batch.removeAttribute( node, 'a' ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( node.getAttribute( 'a' ) ).to.be.undefined; + } ); + + it( 'should remove the attribute from character', () => { + batch.removeAttribute( text, 'a' ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined; + } ); + + it( 'should do nothing if the attribute is not set', () => { + batch.removeAttribute( node, 'b' ); + expect( getOperationsCount() ).to.equal( 0 ); + } ); + + it( 'should be chainable', () => { + const chain = batch.removeAttribute( node, 'a' ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.removeAttribute( node, 'a' ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + } ); + + describe( 'change attribute on range', () => { + beforeEach( () => { + root.insertChildren( 0, [ + new Text( 'xxx', { a: 1 } ), + new Text( 'xxx' ), + new Text( 'xxx', { a: 1 } ), + new Text( 'xxx', { a: 2 } ), + new Text( 'xxx' ), + new Text( 'xxx', { a: 1 } ), + new Element( 'e', { a: 2 }, new Text( 'xxx' ) ), + new Text( 'xxx' ) + ] ); + } ); + + function getRange( startIndex, endIndex ) { + return new Range( + Position.createFromParentAndOffset( root, startIndex ), + Position.createFromParentAndOffset( root, endIndex ) + ); + } + + function getChangesAttrsCount() { + let totalNumber = 0; + + for ( const delta of batch.deltas ) { + for ( const operation of delta.operations ) { + totalNumber += count( operation.range.getItems( { singleCharacters: true } ) ); + } + } + + return totalNumber; + } + + function getCompressedAttrs() { + // default: 111---111222---1112------ + const range = Range.createIn( root ); + + return Array.from( range.getItems( { singleCharacters: true } ) ) + .map( item => item.getAttribute( 'a' ) || '-' ) + .join( '' ); + } + + describe( 'setAttribute', () => { + it( 'should set the attribute on the range', () => { + batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( getChangesAttrsCount() ).to.equal( 3 ); + expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' ); + } ); + + it( 'should split the operations if parts of the range have different attributes', () => { + batch.setAttribute( getRange( 4, 14 ), 'a', 3 ); + expect( getOperationsCount() ).to.equal( 4 ); + expect( getChangesAttrsCount() ).to.equal( 10 ); + expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' ); + } ); + + it( 'should split the operations if parts of the part of the range have the attribute', () => { + batch.setAttribute( getRange( 4, 14 ), 'a', 2 ); + expect( getOperationsCount() ).to.equal( 3 ); + expect( getChangesAttrsCount() ).to.equal( 7 ); + expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' ); + } ); + + it( 'should strip the range if the beginning have the attribute', () => { + batch.setAttribute( getRange( 1, 5 ), 'a', 1 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( getChangesAttrsCount() ).to.equal( 2 ); + expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' ); + } ); + + it( 'should strip the range if the ending have the attribute', () => { + batch.setAttribute( getRange( 13, 17 ), 'a', 1 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( getChangesAttrsCount() ).to.equal( 2 ); + expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' ); + } ); + + it( 'should do nothing if the range has attribute', () => { + batch.setAttribute( getRange( 0, 3 ), 'a', 1 ); + expect( getOperationsCount() ).to.equal( 0 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); + } ); + + it( 'should not check range\'s start position node when creating operations', () => { + const range = new Range( + new Position( root, [ 18, 1 ] ), + new Position( root, [ 19 ] ) + ); + + batch.setAttribute( range, 'a', 1 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( getChangesAttrsCount() ).to.equal( 2 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' ); + } ); + + it( 'should not change elements attribute if range contains closing tag', () => { + const range = new Range( + new Position( root, [ 18, 1 ] ), + new Position( root, [ 21 ] ) + ); + + batch.setAttribute( range, 'a', 1 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( getChangesAttrsCount() ).to.equal( 4 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' ); + } ); + + it( 'should not create an operation if the range contains only closing tag', () => { + const range = new Range( + new Position( root, [ 18, 3 ] ), + new Position( root, [ 19 ] ) + ); + + batch.setAttribute( range, 'a', 3 ); + expect( getOperationsCount() ).to.equal( 0 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); + } ); + + it( 'should not create an operation if is collapsed', () => { + batch.setAttribute( getRange( 3, 3 ), 'a', 1 ); + expect( getOperationsCount() ).to.equal( 0 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); + } ); + + it( 'should create a proper operations for the mixed range', () => { + batch.setAttribute( getRange( 0, 20 ), 'a', 1 ); + expect( getOperationsCount() ).to.equal( 5 ); + expect( getChangesAttrsCount() ).to.equal( 14 ); + expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' ); + } ); + + it( 'should be chainable', () => { + const chain = batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'removeAttribute', () => { + it( 'should remove the attribute on the range', () => { + batch.removeAttribute( getRange( 0, 2 ), 'a' ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( getChangesAttrsCount() ).to.equal( 2 ); + expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' ); + } ); + + it( 'should split the operations if parts of the range have different attributes', () => { + batch.removeAttribute( getRange( 7, 11 ), 'a' ); + expect( getOperationsCount() ).to.equal( 2 ); + expect( getChangesAttrsCount() ).to.equal( 4 ); + expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' ); + } ); + + it( 'should split the operations if parts of the part of the range have no attribute', () => { + batch.removeAttribute( getRange( 1, 7 ), 'a' ); + expect( getOperationsCount() ).to.equal( 2 ); + expect( getChangesAttrsCount() ).to.equal( 3 ); + expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' ); + } ); + + it( 'should strip the range if the beginning have no attribute', () => { + batch.removeAttribute( getRange( 4, 12 ), 'a' ); + expect( getOperationsCount() ).to.equal( 2 ); + expect( getChangesAttrsCount() ).to.equal( 6 ); + expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); + } ); + + it( 'should strip the range if the ending have no attribute', () => { + batch.removeAttribute( getRange( 7, 15 ), 'a' ); + expect( getOperationsCount() ).to.equal( 2 ); + expect( getChangesAttrsCount() ).to.equal( 5 ); + expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' ); + } ); + + it( 'should do nothing if the range has no attribute', () => { + batch.removeAttribute( getRange( 4, 5 ), 'a' ); + expect( getOperationsCount() ).to.equal( 0 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); + } ); + + it( 'should not check range\'s start position node when creating operations', () => { + const range = new Range( + new Position( root, [ 18, 3 ] ), + new Position( root, [ 19 ] ) + ); + + batch.removeAttribute( range, 'a' ); + expect( getOperationsCount() ).to.equal( 0 ); + expect( getChangesAttrsCount() ).to.equal( 0 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); + } ); + + it( 'should not apply operation twice in the range contains opening and closing tags', () => { + batch.removeAttribute( getRange( 18, 22 ), 'a' ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( getChangesAttrsCount() ).to.equal( 1 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---111-------' ); + } ); + + it( 'should not create an operation if range is collapsed', () => { + batch.removeAttribute( getRange( 3, 3 ), 'a' ); + expect( getOperationsCount() ).to.equal( 0 ); + expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); + } ); + + it( 'should create a proper operations for the mixed range', () => { + batch.removeAttribute( getRange( 3, 15 ), 'a' ); + expect( getOperationsCount() ).to.equal( 2 ); + expect( getChangesAttrsCount() ).to.equal( 6 ); + expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); + } ); + + it( 'should be chainable', () => { + const chain = batch.removeAttribute( getRange( 0, 2 ), 'a' ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.removeAttribute( getRange( 0, 2 ), 'a' ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + } ); + + describe( 'change attribute on root element', () => { + describe( 'setAttribute', () => { + it( 'should create the attribute on root', () => { + batch.setAttribute( root, 'b', 2 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( root.getAttribute( 'b' ) ).to.equal( 2 ); + } ); + + it( 'should change the attribute of root', () => { + batch.setAttribute( root, 'a', 2 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( root.getAttribute( 'a' ) ).to.equal( 2 ); + } ); + + it( 'should do nothing if the attribute value is the same', () => { + batch.setAttribute( root, 'a', 1 ); + expect( getOperationsCount() ).to.equal( 1 ); + batch.setAttribute( root, 'a', 1 ); + expect( getOperationsCount() ).to.equal( 1 ); + expect( root.getAttribute( 'a' ) ).to.equal( 1 ); + } ); + } ); + + describe( 'removeAttribute', () => { + it( 'should remove the attribute from root', () => { + batch.setAttribute( root, 'a', 1 ); + batch.removeAttribute( root, 'a' ); + expect( getOperationsCount() ).to.equal( 2 ); + expect( root.getAttribute( 'a' ) ).to.be.undefined; + } ); + + it( 'should do nothing if the attribute is not set', () => { + batch.removeAttribute( root, 'b' ); + expect( getOperationsCount() ).to.equal( 0 ); + } ); + } ); + } ); + + it( 'should not add empty delta to the batch', () => { + const nodeA = new Element( 'p', { a: 1 } ); + const nodeB = new Element( 'p', { b: 2 } ); + root.insertChildren( 0, [ nodeA, nodeB ] ); + + batch.setAttribute( nodeA, 'a', 1 ); + + expect( batch.deltas.length ).to.equal( 0 ); + + batch.removeAttribute( Range.createIn( root ), 'x' ); + + expect( batch.deltas.length ).to.equal( 0 ); + } ); + } ); + + describe( 'merge()', () => { + let doc, root, p1, p2, batch; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + p1 = new Element( 'p', { key1: 'value1' }, new Text( 'foo' ) ); + p2 = new Element( 'p', { key2: 'value2' }, new Text( 'bar' ) ); + + root.insertChildren( 0, [ p1, p2 ] ); + } ); + + it( 'should merge foo and bar into foobar', () => { + doc.batch().merge( new Position( root, [ 1 ] ) ); + + expect( root.maxOffset ).to.equal( 1 ); + expect( root.getChild( 0 ).name ).to.equal( 'p' ); + expect( root.getChild( 0 ).maxOffset ).to.equal( 6 ); + expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 ); + expect( root.getChild( 0 ).getAttribute( 'key1' ) ).to.equal( 'value1' ); + expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' ); + } ); + + it( 'should throw if there is no element after', () => { + expect( () => { + doc.batch().merge( new Position( root, [ 2 ] ) ); + } ).to.throw( CKEditorError, /^batch-merge-no-element-after/ ); + } ); + + it( 'should throw if there is no element before', () => { + expect( () => { + doc.batch().merge( new Position( root, [ 0, 2 ] ) ); + } ).to.throw( CKEditorError, /^batch-merge-no-element-before/ ); + } ); + + it( 'should be chainable', () => { + batch = doc.batch(); + + const chain = batch.merge( new Position( root, [ 1 ] ) ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch = doc.batch().merge( new Position( root, [ 1 ] ) ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'move()', () => { + let doc, root, div, p, batch, chain; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + div = new Element( 'div', [], new Text( 'foobar' ) ); + p = new Element( 'p', [], new Text( 'abcxyz' ) ); + + div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] ); + div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] ); + + root.insertChildren( 0, [ div, p ] ); + + batch = doc.batch(); + } ); + + it( 'should move specified node', () => { + batch.move( div, new Position( root, [ 2 ] ) ); + + expect( root.maxOffset ).to.equal( 2 ); + expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); + expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'PggggPfoobarPhhhhP' ); + } ); + + it( 'should move flat range of nodes', () => { + const range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) ); + batch.move( range, new Position( root, [ 1, 3 ] ) ); + + expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggggPfoPhhhhP' ); + expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' ); + } ); + + it( 'should throw if given range is not flat', () => { + const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) ); + + expect( () => { + doc.batch().move( notFlatRange, new Position( root, [ 1, 3 ] ) ); + } ).to.throw( CKEditorError, /^batch-move-range-not-flat/ ); + } ); + + it( 'should be chainable', () => { + chain = batch.move( div, new Position( root, [ 1, 3 ] ) ); + + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.move( div, new Position( root, [ 2 ] ) ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'remove()', () => { + let doc, root, div, p, batch, chain, range; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + div = new Element( 'div', [], new Text( 'foobar' ) ); + p = new Element( 'p', [], new Text( 'abcxyz' ) ); + + div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] ); + div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] ); + + root.insertChildren( 0, [ div, p ] ); + + batch = doc.batch(); + + // Range starts in ROOT > DIV > P > gg|gg. + // Range ends in ROOT > DIV > ...|ar. + range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) ); + } ); + + it( 'should remove specified node', () => { + batch.remove( div ); + + expect( root.maxOffset ).to.equal( 1 ); + expect( root.childCount ).to.equal( 1 ); + expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); + } ); + + it( 'should remove any range of nodes', () => { + batch.remove( range ); + + expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' ); + expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' ); + } ); + + it( 'should create minimal number of remove deltas, each with only one operation', () => { + batch.remove( range ); + + expect( batch.deltas.length ).to.equal( 2 ); + expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 ); + expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 ); + } ); + + it( 'should be chainable', () => { + chain = batch.remove( range ); + + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.remove( div ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'rename()', () => { + let doc, root, batch, chain; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + const p = new Element( 'p', null, new Text( 'abc' ) ); + root.appendChildren( p ); + + batch = doc.batch(); + + chain = batch.rename( p, 'h' ); + } ); + + it( 'should rename given element', () => { + expect( root.maxOffset ).to.equal( 1 ); + expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' ); + expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' ); + } ); + + it( 'should throw if not an Element instance is passed', () => { + expect( () => { + batch.rename( new Text( 'abc' ), 'h' ); + } ).to.throw( CKEditorError, /^batch-rename-not-element-instance/ ); + } ); + + it( 'should be chainable', () => { + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.rename( root.getChild( 0 ), 'p' ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.alwaysCalledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'split()', () => { + let doc, root, p; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + p = new Element( 'p', { key: 'value' }, new Text( 'foobar' ) ); + + root.insertChildren( 0, p ); + } ); + + it( 'should split foobar to foo and bar', () => { + doc.batch().split( new Position( root, [ 0, 3 ] ) ); + + expect( root.maxOffset ).to.equal( 2 ); + + expect( root.getChild( 0 ).name ).to.equal( 'p' ); + expect( root.getChild( 0 ).maxOffset ).to.equal( 3 ); + expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 ); + expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' ); + expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' ); + + expect( root.getChild( 1 ).name ).to.equal( 'p' ); + expect( root.getChild( 1 ).maxOffset ).to.equal( 3 ); + expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 ); + expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' ); + expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'bar' ); + } ); + + it( 'should create an empty paragraph if we split at the end', () => { + doc.batch().split( new Position( root, [ 0, 6 ] ) ); + + expect( root.maxOffset ).to.equal( 2 ); + + expect( root.getChild( 0 ).name ).to.equal( 'p' ); + expect( root.getChild( 0 ).maxOffset ).to.equal( 6 ); + expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 ); + expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' ); + expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' ); + + expect( root.getChild( 1 ).name ).to.equal( 'p' ); + expect( root.getChild( 1 ).maxOffset ).to.equal( 0 ); + expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 ); + expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' ); + } ); + + it( 'should throw if we try to split a root', () => { + expect( () => { + doc.batch().split( new Position( root, [ 0 ] ) ); + } ).to.throw( CKEditorError, /^batch-split-root/ ); + } ); + + it( 'should be chainable', () => { + const batch = doc.batch(); + + const chain = batch.split( new Position( root, [ 0, 3 ] ) ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + const batch = doc.batch().split( new Position( root, [ 0, 3 ] ) ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'wrap()', () => { + let doc, root, range; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + root.insertChildren( 0, new Text( 'foobar' ) ); + + range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) ); + } ); + + it( 'should wrap flat range with given element', () => { + const p = new Element( 'p' ); + doc.batch().wrap( range, p ); + + expect( root.maxOffset ).to.equal( 5 ); + expect( root.getChild( 0 ).data ).to.equal( 'fo' ); + expect( root.getChild( 1 ) ).to.equal( p ); + expect( p.getChild( 0 ).data ).to.equal( 'ob' ); + expect( root.getChild( 2 ).data ).to.equal( 'ar' ); + } ); + + it( 'should wrap flat range with an element of given name', () => { + doc.batch().wrap( range, 'p' ); + + expect( root.maxOffset ).to.equal( 5 ); + expect( root.getChild( 0 ).data ).to.equal( 'fo' ); + expect( root.getChild( 1 ).name ).to.equal( 'p' ); + expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'ob' ); + expect( root.getChild( 2 ).data ).to.equal( 'ar' ); + } ); + + it( 'should throw if range to wrap is not flat', () => { + root.insertChildren( 1, [ new Element( 'p', [], new Text( 'xyz' ) ) ] ); + const notFlatRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 6, 2 ] ) ); + + expect( () => { + doc.batch().wrap( notFlatRange, 'p' ); + } ).to.throw( CKEditorError, /^batch-wrap-range-not-flat/ ); + } ); + + it( 'should throw if element to wrap with has children', () => { + const p = new Element( 'p', [], new Text( 'a' ) ); + + expect( () => { + doc.batch().wrap( range, p ); + } ).to.throw( CKEditorError, /^batch-wrap-element-not-empty/ ); + } ); + + it( 'should throw if element to wrap with has children', () => { + const p = new Element( 'p' ); + root.insertChildren( 0, p ); + + expect( () => { + doc.batch().wrap( range, p ); + } ).to.throw( CKEditorError, /^batch-wrap-element-attached/ ); + } ); + + it( 'should be chainable', () => { + const batch = doc.batch(); + + const chain = batch.wrap( range, 'p' ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + const batch = doc.batch().wrap( range, 'p' ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'unwrap()', () => { + let doc, root, p; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + + p = new Element( 'p', [], new Text( 'xyz' ) ); + root.insertChildren( 0, [ new Text( 'a' ), p, new Text( 'b' ) ] ); + } ); + + it( 'should unwrap given element', () => { + doc.batch().unwrap( p ); + + expect( root.maxOffset ).to.equal( 5 ); + expect( root.getChild( 0 ).data ).to.equal( 'axyzb' ); + } ); + + it( 'should throw if element to unwrap has no parent', () => { + const element = new Element( 'p' ); + + expect( () => { + doc.batch().unwrap( element ); + } ).to.throw( CKEditorError, /^batch-unwrap-element-no-parent/ ); + } ); + + it( 'should be chainable', () => { + const batch = doc.batch(); + + const chain = batch.unwrap( p ); + expect( chain ).to.equal( batch ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + const batch = doc.batch().unwrap( p ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + } ); + + describe( 'setMarker()', () => { + let doc, root, range; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + root.appendChildren( new Text( 'foo' ) ); + range = Range.createIn( root ); + } ); + + it( 'should add marker to the document marker collection', () => { + doc.batch().setMarker( 'name', range ); + + expect( doc.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true; + } ); + + it( 'should update marker in the document marker collection', () => { + doc.batch().setMarker( 'name', range ); + + const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 ); + doc.batch().setMarker( 'name', range2 ); + + expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true; + } ); + + it( 'should accept marker instance', () => { + doc.batch().setMarker( 'name', range ); + const marker = doc.markers.get( 'name' ); + const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 ); + + const batch = doc.batch().setMarker( marker, range2 ); + const op = batch.deltas[ 0 ].operations[ 0 ]; + + expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true; + expect( op.oldRange.isEqual( range ) ).to.be.true; + expect( op.newRange.isEqual( range2 ) ).to.be.true; + } ); + + it( 'should accept empty range parameter if marker instance is passed', () => { + const marker = doc.markers.set( 'name', range ); + + sinon.spy( doc, 'fire' ); + + doc.on( 'change', ( evt, type, changes ) => { + if ( type == 'marker' ) { + expect( changes.type ).to.equal( 'set' ); + expect( changes.name ).to.equal( 'name' ); + } + } ); + + const batch = doc.batch().setMarker( marker ); + const op = batch.deltas[ 0 ].operations[ 0 ]; + + expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true; + expect( op.oldRange ).to.be.null; + expect( op.newRange.isEqual( range ) ).to.be.true; + } ); + + it( 'should throw if marker with given name does not exist and range is not passed', () => { + expect( () => { + doc.batch().setMarker( 'name' ); + } ).to.throw( CKEditorError, /^batch-setMarker-no-range/ ); + } ); + + it( 'should be chainable', () => { + const batch = doc.batch(); + const chain = batch.setMarker( 'name', range ); + + expect( chain ).to.equal( batch ); + } ); + } ); + + describe( 'removeMarker()', () => { + let doc, root, range; + + beforeEach( () => { + doc = new Document(); + root = doc.createRoot(); + root.appendChildren( new Text( 'foo' ) ); + range = Range.createIn( root ); + } ); + + it( 'should remove marker from the document marker collection', () => { + doc.batch().setMarker( 'name', range ); + doc.batch().removeMarker( 'name' ); + + expect( doc.markers.get( 'name' ) ).to.be.null; + } ); + + it( 'should throw when trying to remove non existing marker', () => { + expect( () => { + doc.batch().removeMarker( 'name' ); + } ).to.throw( CKEditorError, /^batch-removeMarker-no-marker/ ); + } ); + + it( 'should accept marker instance', () => { + doc.batch().setMarker( 'name', range ); + const marker = doc.markers.get( 'name' ); + + doc.batch().removeMarker( marker ); + + expect( doc.markers.get( 'name' ) ).to.be.null; + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + const batch = doc.batch().setMarker( 'name', range ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; } ); } ); } ); diff --git a/tests/model/delta/attributedelta.js b/tests/model/delta/attributedelta.js index b059ce174..442a17a4c 100644 --- a/tests/model/delta/attributedelta.js +++ b/tests/model/delta/attributedelta.js @@ -3,412 +3,14 @@ * For licensing, see LICENSE.md. */ -import count from '@ckeditor/ckeditor5-utils/src/count'; import Document from '../../../src/model/document'; -import Text from '../../../src/model/text'; import Range from '../../../src/model/range'; import Position from '../../../src/model/position'; -import Element from '../../../src/model/element'; import { default as AttributeDelta, RootAttributeDelta } from '../../../src/model/delta/attributedelta'; import AttributeOperation from '../../../src/model/operation/attributeoperation'; import NoOperation from '../../../src/model/operation/nooperation'; import { jsonParseStringify } from '../../../tests/model/_utils/utils'; -describe( 'Batch', () => { - let batch, doc, root; - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - batch = doc.batch(); - } ); - - function getOperationsCount() { - let totalNumber = 0; - - for ( const delta of batch.deltas ) { - totalNumber += count( delta.operations ); - } - - return totalNumber; - } - - describe( 'change attribute on node', () => { - let node, text; - - beforeEach( () => { - node = new Element( 'p', { a: 1 } ); - text = new Text( 'c', { a: 1 } ); - - root.insertChildren( 0, [ node, text ] ); - } ); - - describe( 'setAttribute', () => { - it( 'should create the attribute on element', () => { - batch.setAttribute( node, 'b', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( node.getAttribute( 'b' ) ).to.equal( 2 ); - } ); - - it( 'should change the attribute of element', () => { - batch.setAttribute( node, 'a', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( node.getAttribute( 'a' ) ).to.equal( 2 ); - } ); - - it( 'should create the attribute on text node', () => { - batch.setAttribute( text, 'b', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 ); - } ); - - it( 'should change the attribute of text node', () => { - batch.setAttribute( text, 'a', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 ); - } ); - - it( 'should do nothing if the attribute value is the same', () => { - batch.setAttribute( node, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 0 ); - expect( node.getAttribute( 'a' ) ).to.equal( 1 ); - } ); - - it( 'should be chainable', () => { - const chain = batch.setAttribute( node, 'b', 2 ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.setAttribute( node, 'b', 2 ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); - - describe( 'removeAttribute', () => { - it( 'should remove the attribute from element', () => { - batch.removeAttribute( node, 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( node.getAttribute( 'a' ) ).to.be.undefined; - } ); - - it( 'should remove the attribute from character', () => { - batch.removeAttribute( text, 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined; - } ); - - it( 'should do nothing if the attribute is not set', () => { - batch.removeAttribute( node, 'b' ); - expect( getOperationsCount() ).to.equal( 0 ); - } ); - - it( 'should be chainable', () => { - const chain = batch.removeAttribute( node, 'a' ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.removeAttribute( node, 'a' ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); - } ); - - describe( 'change attribute on range', () => { - beforeEach( () => { - root.insertChildren( 0, [ - new Text( 'xxx', { a: 1 } ), - new Text( 'xxx' ), - new Text( 'xxx', { a: 1 } ), - new Text( 'xxx', { a: 2 } ), - new Text( 'xxx' ), - new Text( 'xxx', { a: 1 } ), - new Element( 'e', { a: 2 }, new Text( 'xxx' ) ), - new Text( 'xxx' ) - ] ); - } ); - - function getRange( startIndex, endIndex ) { - return new Range( - Position.createFromParentAndOffset( root, startIndex ), - Position.createFromParentAndOffset( root, endIndex ) - ); - } - - function getChangesAttrsCount() { - let totalNumber = 0; - - for ( const delta of batch.deltas ) { - for ( const operation of delta.operations ) { - totalNumber += count( operation.range.getItems( { singleCharacters: true } ) ); - } - } - - return totalNumber; - } - - function getCompressedAttrs() { - // default: 111---111222---1112------ - const range = Range.createIn( root ); - - return Array.from( range.getItems( { singleCharacters: true } ) ) - .map( item => item.getAttribute( 'a' ) || '-' ) - .join( '' ); - } - - describe( 'setAttribute', () => { - it( 'should set the attribute on the range', () => { - batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( getChangesAttrsCount() ).to.equal( 3 ); - expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' ); - } ); - - it( 'should split the operations if parts of the range have different attributes', () => { - batch.setAttribute( getRange( 4, 14 ), 'a', 3 ); - expect( getOperationsCount() ).to.equal( 4 ); - expect( getChangesAttrsCount() ).to.equal( 10 ); - expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' ); - } ); - - it( 'should split the operations if parts of the part of the range have the attribute', () => { - batch.setAttribute( getRange( 4, 14 ), 'a', 2 ); - expect( getOperationsCount() ).to.equal( 3 ); - expect( getChangesAttrsCount() ).to.equal( 7 ); - expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' ); - } ); - - it( 'should strip the range if the beginning have the attribute', () => { - batch.setAttribute( getRange( 1, 5 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( getChangesAttrsCount() ).to.equal( 2 ); - expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' ); - } ); - - it( 'should strip the range if the ending have the attribute', () => { - batch.setAttribute( getRange( 13, 17 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( getChangesAttrsCount() ).to.equal( 2 ); - expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' ); - } ); - - it( 'should do nothing if the range has attribute', () => { - batch.setAttribute( getRange( 0, 3 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 0 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); - } ); - - it( 'should not check range\'s start position node when creating operations', () => { - const range = new Range( - new Position( root, [ 18, 1 ] ), - new Position( root, [ 19 ] ) - ); - - batch.setAttribute( range, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( getChangesAttrsCount() ).to.equal( 2 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' ); - } ); - - it( 'should not change elements attribute if range contains closing tag', () => { - const range = new Range( - new Position( root, [ 18, 1 ] ), - new Position( root, [ 21 ] ) - ); - - batch.setAttribute( range, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( getChangesAttrsCount() ).to.equal( 4 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' ); - } ); - - it( 'should not create an operation if the range contains only closing tag', () => { - const range = new Range( - new Position( root, [ 18, 3 ] ), - new Position( root, [ 19 ] ) - ); - - batch.setAttribute( range, 'a', 3 ); - expect( getOperationsCount() ).to.equal( 0 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); - } ); - - it( 'should not create an operation if is collapsed', () => { - batch.setAttribute( getRange( 3, 3 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 0 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); - } ); - - it( 'should create a proper operations for the mixed range', () => { - batch.setAttribute( getRange( 0, 20 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 5 ); - expect( getChangesAttrsCount() ).to.equal( 14 ); - expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' ); - } ); - - it( 'should be chainable', () => { - const chain = batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); - - describe( 'removeAttribute', () => { - it( 'should remove the attribute on the range', () => { - batch.removeAttribute( getRange( 0, 2 ), 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( getChangesAttrsCount() ).to.equal( 2 ); - expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' ); - } ); - - it( 'should split the operations if parts of the range have different attributes', () => { - batch.removeAttribute( getRange( 7, 11 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); - expect( getChangesAttrsCount() ).to.equal( 4 ); - expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' ); - } ); - - it( 'should split the operations if parts of the part of the range have no attribute', () => { - batch.removeAttribute( getRange( 1, 7 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); - expect( getChangesAttrsCount() ).to.equal( 3 ); - expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' ); - } ); - - it( 'should strip the range if the beginning have no attribute', () => { - batch.removeAttribute( getRange( 4, 12 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); - expect( getChangesAttrsCount() ).to.equal( 6 ); - expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); - } ); - - it( 'should strip the range if the ending have no attribute', () => { - batch.removeAttribute( getRange( 7, 15 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); - expect( getChangesAttrsCount() ).to.equal( 5 ); - expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' ); - } ); - - it( 'should do nothing if the range has no attribute', () => { - batch.removeAttribute( getRange( 4, 5 ), 'a' ); - expect( getOperationsCount() ).to.equal( 0 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); - } ); - - it( 'should not check range\'s start position node when creating operations', () => { - const range = new Range( - new Position( root, [ 18, 3 ] ), - new Position( root, [ 19 ] ) - ); - - batch.removeAttribute( range, 'a' ); - expect( getOperationsCount() ).to.equal( 0 ); - expect( getChangesAttrsCount() ).to.equal( 0 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); - } ); - - it( 'should not apply operation twice in the range contains opening and closing tags', () => { - batch.removeAttribute( getRange( 18, 22 ), 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( getChangesAttrsCount() ).to.equal( 1 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---111-------' ); - } ); - - it( 'should not create an operation if range is collapsed', () => { - batch.removeAttribute( getRange( 3, 3 ), 'a' ); - expect( getOperationsCount() ).to.equal( 0 ); - expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); - } ); - - it( 'should create a proper operations for the mixed range', () => { - batch.removeAttribute( getRange( 3, 15 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); - expect( getChangesAttrsCount() ).to.equal( 6 ); - expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); - } ); - - it( 'should be chainable', () => { - const chain = batch.removeAttribute( getRange( 0, 2 ), 'a' ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.removeAttribute( getRange( 0, 2 ), 'a' ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); - } ); - - describe( 'change attribute on root element', () => { - describe( 'setAttribute', () => { - it( 'should create the attribute on root', () => { - batch.setAttribute( root, 'b', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( root.getAttribute( 'b' ) ).to.equal( 2 ); - } ); - - it( 'should change the attribute of root', () => { - batch.setAttribute( root, 'a', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( root.getAttribute( 'a' ) ).to.equal( 2 ); - } ); - - it( 'should do nothing if the attribute value is the same', () => { - batch.setAttribute( root, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); - batch.setAttribute( root, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); - expect( root.getAttribute( 'a' ) ).to.equal( 1 ); - } ); - } ); - - describe( 'removeAttribute', () => { - it( 'should remove the attribute from root', () => { - batch.setAttribute( root, 'a', 1 ); - batch.removeAttribute( root, 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); - expect( root.getAttribute( 'a' ) ).to.be.undefined; - } ); - - it( 'should do nothing if the attribute is not set', () => { - batch.removeAttribute( root, 'b' ); - expect( getOperationsCount() ).to.equal( 0 ); - } ); - } ); - } ); - - it( 'should not add empty delta to the batch', () => { - const nodeA = new Element( 'p', { a: 1 } ); - const nodeB = new Element( 'p', { b: 2 } ); - root.insertChildren( 0, [ nodeA, nodeB ] ); - - batch.setAttribute( nodeA, 'a', 1 ); - - expect( batch.deltas.length ).to.equal( 0 ); - - batch.removeAttribute( Range.createIn( root ), 'x' ); - - expect( batch.deltas.length ).to.equal( 0 ); - } ); -} ); - describe( 'AttributeDelta', () => { let doc, root, delta; diff --git a/tests/model/delta/insertdelta.js b/tests/model/delta/insertdelta.js index d595a1429..9ee9467cf 100644 --- a/tests/model/delta/insertdelta.js +++ b/tests/model/delta/insertdelta.js @@ -5,102 +5,14 @@ import Document from '../../../src/model/document'; import Element from '../../../src/model/element'; -import DocumentFragment from '../../../src/model/documentfragment'; -import Text from '../../../src/model/text'; import Position from '../../../src/model/position'; -import Range from '../../../src/model/range'; import InsertOperation from '../../../src/model/operation/insertoperation'; -import MarkerOperation from '../../../src/model/operation/markeroperation'; import InsertDelta from '../../../src/model/delta/insertdelta'; import RemoveDelta from '../../../src/model/delta/removedelta'; import RemoveOperation from '../../../src/model/operation/removeoperation'; -import { stringify } from '../../../src/dev-utils/model'; - -describe( 'Batch', () => { - let doc, root, batch, p, ul, chain; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - root.insertChildren( 0, new Text( 'abc' ) ); - - batch = doc.batch(); - - p = new Element( 'p' ); - ul = new Element( 'ul' ); - - chain = batch.insert( new Position( root, [ 2 ] ), [ p, ul ] ); - } ); - - describe( 'insert', () => { - it( 'should insert given nodes at given position', () => { - expect( root.childCount ).to.equal( 4 ); - expect( root.maxOffset ).to.equal( 5 ); - expect( root.getChild( 1 ) ).to.equal( p ); - expect( root.getChild( 2 ) ).to.equal( ul ); - } ); - - it( 'should be chainable', () => { - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.insert( new Position( root, [ 2 ] ), [ p, ul ] ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - - it( 'should transfer markers from given DocumentFragment', () => { - const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] ); - const marker = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 5 ] ) ); - - documentFragment.markers.set( 'marker', marker ); - - batch.insert( new Position( root, [ 3, 0 ] ), documentFragment ); - - expect( Array.from( doc.markers ).length ).to.equal( 1 ); - expect( stringify( root, doc.markers.get( 'marker' ).getRange() ) ).to.equal( 'ab

c' ); - } ); - - it( 'should set each marker as separate operation', () => { - sinon.spy( doc, 'applyOperation' ); - - const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] ); - const marker1 = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 2 ] ) ); - const marker2 = new Range( new Position( documentFragment, [ 0, 5 ] ), new Position( documentFragment, [ 0, 6 ] ) ); - - documentFragment.markers.set( 'marker1', marker1 ); - documentFragment.markers.set( 'marker2', marker2 ); - - batch.insert( new Position( root, [ 3, 0 ] ), documentFragment ); - - expect( doc.applyOperation.calledThrice ); - expect( doc.applyOperation.firstCall.calledWith( sinon.match( operation => operation instanceof InsertOperation ) ) ); - expect( doc.applyOperation.secondCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); - expect( doc.applyOperation.thirdCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); - } ); - - it( 'should not create a delta and an operation if no nodes were inserted', () => { - sinon.spy( doc, 'applyOperation' ); - - batch = doc.batch(); - - batch.insert( new Position( root, [ 0 ] ), [] ); - - expect( batch.deltas.length ).to.equal( 0 ); - expect( doc.applyOperation.called ).to.be.false; - } ); - } ); -} ); - describe( 'InsertDelta', () => { let insertDelta, doc, root; diff --git a/tests/model/delta/markerdelta.js b/tests/model/delta/markerdelta.js index b14a97534..87673a073 100644 --- a/tests/model/delta/markerdelta.js +++ b/tests/model/delta/markerdelta.js @@ -5,121 +5,10 @@ import Document from '../../../src/model/document'; import Range from '../../../src/model/range'; -import Text from '../../../src/model/text'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import MarkerDelta from '../../../src/model/delta/markerdelta'; import MarkerOperation from '../../../src/model/operation/markeroperation'; -describe( 'Batch', () => { - let doc, root, range; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - root.appendChildren( new Text( 'foo' ) ); - range = Range.createIn( root ); - } ); - - describe( 'setMarker', () => { - it( 'should add marker to the document marker collection', () => { - doc.batch().setMarker( 'name', range ); - - expect( doc.markers.get( 'name' ).getRange().isEqual( range ) ).to.be.true; - } ); - - it( 'should update marker in the document marker collection', () => { - doc.batch().setMarker( 'name', range ); - - const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 ); - doc.batch().setMarker( 'name', range2 ); - - expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true; - } ); - - it( 'should accept marker instance', () => { - doc.batch().setMarker( 'name', range ); - const marker = doc.markers.get( 'name' ); - const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 ); - - const batch = doc.batch().setMarker( marker, range2 ); - const op = batch.deltas[ 0 ].operations[ 0 ]; - - expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true; - expect( op.oldRange.isEqual( range ) ).to.be.true; - expect( op.newRange.isEqual( range2 ) ).to.be.true; - } ); - - it( 'should accept empty range parameter if marker instance is passed', () => { - const marker = doc.markers.set( 'name', range ); - - sinon.spy( doc, 'fire' ); - - doc.on( 'change', ( evt, type, changes ) => { - if ( type == 'marker' ) { - expect( changes.type ).to.equal( 'set' ); - expect( changes.name ).to.equal( 'name' ); - } - } ); - - const batch = doc.batch().setMarker( marker ); - const op = batch.deltas[ 0 ].operations[ 0 ]; - - expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true; - expect( op.oldRange ).to.be.null; - expect( op.newRange.isEqual( range ) ).to.be.true; - } ); - - it( 'should throw if marker with given name does not exist and range is not passed', () => { - expect( () => { - doc.batch().setMarker( 'name' ); - } ).to.throw( CKEditorError, /^batch-setMarker-no-range/ ); - } ); - } ); - - describe( 'removeMarker', () => { - it( 'should remove marker from the document marker collection', () => { - doc.batch().setMarker( 'name', range ); - doc.batch().removeMarker( 'name' ); - - expect( doc.markers.get( 'name' ) ).to.be.null; - } ); - - it( 'should throw when trying to remove non existing marker', () => { - expect( () => { - doc.batch().removeMarker( 'name' ); - } ).to.throw( CKEditorError, /^batch-removeMarker-no-marker/ ); - } ); - - it( 'should accept marker instance', () => { - doc.batch().setMarker( 'name', range ); - const marker = doc.markers.get( 'name' ); - - doc.batch().removeMarker( marker ); - - expect( doc.markers.get( 'name' ) ).to.be.null; - } ); - } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - const chain = batch.setMarker( 'name', range ); - - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().setMarker( 'name', range ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); -} ); - describe( 'MarkerDelta', () => { let markerDelta, doc, root, range; diff --git a/tests/model/delta/mergedelta.js b/tests/model/delta/mergedelta.js index 553f6ff55..96c123a3c 100644 --- a/tests/model/delta/mergedelta.js +++ b/tests/model/delta/mergedelta.js @@ -5,9 +5,6 @@ import Document from '../../../src/model/document'; import Position from '../../../src/model/position'; -import Element from '../../../src/model/element'; -import Text from '../../../src/model/text'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import MergeDelta from '../../../src/model/delta/mergedelta'; import SplitDelta from '../../../src/model/delta/splitdelta'; @@ -16,65 +13,6 @@ import MoveOperation from '../../../src/model/operation/moveoperation'; import RemoveOperation from '../../../src/model/operation/removeoperation'; import ReinsertOperation from '../../../src/model/operation/reinsertoperation'; -import count from '@ckeditor/ckeditor5-utils/src/count'; - -describe( 'Batch', () => { - let doc, root, p1, p2, batch; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - p1 = new Element( 'p', { key1: 'value1' }, new Text( 'foo' ) ); - p2 = new Element( 'p', { key2: 'value2' }, new Text( 'bar' ) ); - - root.insertChildren( 0, [ p1, p2 ] ); - } ); - - describe( 'merge', () => { - it( 'should merge foo and bar into foobar', () => { - doc.batch().merge( new Position( root, [ 1 ] ) ); - - expect( root.maxOffset ).to.equal( 1 ); - expect( root.getChild( 0 ).name ).to.equal( 'p' ); - expect( root.getChild( 0 ).maxOffset ).to.equal( 6 ); - expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 ); - expect( root.getChild( 0 ).getAttribute( 'key1' ) ).to.equal( 'value1' ); - expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' ); - } ); - - it( 'should throw if there is no element after', () => { - expect( () => { - doc.batch().merge( new Position( root, [ 2 ] ) ); - } ).to.throw( CKEditorError, /^batch-merge-no-element-after/ ); - } ); - - it( 'should throw if there is no element before', () => { - expect( () => { - doc.batch().merge( new Position( root, [ 0, 2 ] ) ); - } ).to.throw( CKEditorError, /^batch-merge-no-element-before/ ); - } ); - - it( 'should be chainable', () => { - batch = doc.batch(); - - const chain = batch.merge( new Position( root, [ 1 ] ) ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch = doc.batch().merge( new Position( root, [ 1 ] ) ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'MergeDelta', () => { let mergeDelta, doc, root; diff --git a/tests/model/delta/movedelta.js b/tests/model/delta/movedelta.js index c17177a1c..f90c6b412 100644 --- a/tests/model/delta/movedelta.js +++ b/tests/model/delta/movedelta.js @@ -3,79 +3,12 @@ * For licensing, see LICENSE.md. */ -import { getNodesAndText } from '../../../tests/model/_utils/utils'; import Document from '../../../src/model/document'; import Position from '../../../src/model/position'; -import Range from '../../../src/model/range'; -import Element from '../../../src/model/element'; -import Text from '../../../src/model/text'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import MoveDelta from '../../../src/model/delta/movedelta'; import MoveOperation from '../../../src/model/operation/moveoperation'; -describe( 'Batch', () => { - let doc, root, div, p, batch, chain; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - div = new Element( 'div', [], new Text( 'foobar' ) ); - p = new Element( 'p', [], new Text( 'abcxyz' ) ); - - div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] ); - div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] ); - - root.insertChildren( 0, [ div, p ] ); - - batch = doc.batch(); - } ); - - describe( 'move', () => { - it( 'should move specified node', () => { - batch.move( div, new Position( root, [ 2 ] ) ); - - expect( root.maxOffset ).to.equal( 2 ); - expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); - expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'PggggPfoobarPhhhhP' ); - } ); - - it( 'should move flat range of nodes', () => { - const range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) ); - batch.move( range, new Position( root, [ 1, 3 ] ) ); - - expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggggPfoPhhhhP' ); - expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' ); - } ); - - it( 'should throw if given range is not flat', () => { - const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) ); - - expect( () => { - doc.batch().move( notFlatRange, new Position( root, [ 1, 3 ] ) ); - } ).to.throw( CKEditorError, /^batch-move-range-not-flat/ ); - } ); - - it( 'should be chainable', () => { - chain = batch.move( div, new Position( root, [ 1, 3 ] ) ); - - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.move( div, new Position( root, [ 2 ] ) ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'MoveDelta', () => { let moveDelta, doc, root; diff --git a/tests/model/delta/removedelta.js b/tests/model/delta/removedelta.js index 6509cbcff..d48c38349 100644 --- a/tests/model/delta/removedelta.js +++ b/tests/model/delta/removedelta.js @@ -3,79 +3,8 @@ * For licensing, see LICENSE.md. */ -import { getNodesAndText } from '../../../tests/model/_utils/utils'; -import Document from '../../../src/model/document'; -import Position from '../../../src/model/position'; -import Range from '../../../src/model/range'; -import Element from '../../../src/model/element'; -import Text from '../../../src/model/text'; import RemoveDelta from '../../../src/model/delta/removedelta'; -describe( 'Batch', () => { - let doc, root, div, p, batch, chain, range; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - div = new Element( 'div', [], new Text( 'foobar' ) ); - p = new Element( 'p', [], new Text( 'abcxyz' ) ); - - div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] ); - div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] ); - - root.insertChildren( 0, [ div, p ] ); - - batch = doc.batch(); - - // Range starts in ROOT > DIV > P > gg|gg. - // Range ends in ROOT > DIV > ...|ar. - range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) ); - } ); - - describe( 'remove', () => { - it( 'should remove specified node', () => { - batch.remove( div ); - - expect( root.maxOffset ).to.equal( 1 ); - expect( root.childCount ).to.equal( 1 ); - expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); - } ); - - it( 'should remove any range of nodes', () => { - batch.remove( range ); - - expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' ); - expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' ); - } ); - - it( 'should create minimal number of remove deltas, each with only one operation', () => { - batch.remove( range ); - - expect( batch.deltas.length ).to.equal( 2 ); - expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 ); - expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 ); - } ); - - it( 'should be chainable', () => { - chain = batch.remove( range ); - - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.remove( div ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'RemoveDelta', () => { it( 'should provide proper className', () => { expect( RemoveDelta.className ).to.equal( 'engine.model.delta.RemoveDelta' ); diff --git a/tests/model/delta/renamedelta.js b/tests/model/delta/renamedelta.js index 97c10fefc..991bd214b 100644 --- a/tests/model/delta/renamedelta.js +++ b/tests/model/delta/renamedelta.js @@ -6,55 +6,9 @@ import Document from '../../../src/model/document'; import Element from '../../../src/model/element'; import Text from '../../../src/model/text'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import RenameDelta from '../../../src/model/delta/renamedelta'; -describe( 'Batch', () => { - let doc, root, batch, chain; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - const p = new Element( 'p', null, new Text( 'abc' ) ); - root.appendChildren( p ); - - batch = doc.batch(); - - chain = batch.rename( p, 'h' ); - } ); - - describe( 'rename', () => { - it( 'should rename given element', () => { - expect( root.maxOffset ).to.equal( 1 ); - expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' ); - expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' ); - } ); - - it( 'should throw if not an Element instance is passed', () => { - expect( () => { - batch.rename( new Text( 'abc' ), 'h' ); - } ).to.throw( CKEditorError, /^batch-rename-not-element-instance/ ); - } ); - - it( 'should be chainable', () => { - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.rename( root.getChild( 0 ), 'p' ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.alwaysCalledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'RenameDelta', () => { let renameDelta, doc, root; diff --git a/tests/model/delta/splitdelta.js b/tests/model/delta/splitdelta.js index 9e39e4df7..2503d1cc3 100644 --- a/tests/model/delta/splitdelta.js +++ b/tests/model/delta/splitdelta.js @@ -6,8 +6,6 @@ import Document from '../../../src/model/document'; import Position from '../../../src/model/position'; import Element from '../../../src/model/element'; -import Text from '../../../src/model/text'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import MergeDelta from '../../../src/model/delta/mergedelta'; import SplitDelta from '../../../src/model/delta/splitdelta'; @@ -17,82 +15,6 @@ import MoveOperation from '../../../src/model/operation/moveoperation'; import NoOperation from '../../../src/model/operation/nooperation'; import RemoveOperation from '../../../src/model/operation/removeoperation'; -import count from '@ckeditor/ckeditor5-utils/src/count'; - -describe( 'Batch', () => { - let doc, root, p; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - p = new Element( 'p', { key: 'value' }, new Text( 'foobar' ) ); - - root.insertChildren( 0, p ); - } ); - - describe( 'split', () => { - it( 'should split foobar to foo and bar', () => { - doc.batch().split( new Position( root, [ 0, 3 ] ) ); - - expect( root.maxOffset ).to.equal( 2 ); - - expect( root.getChild( 0 ).name ).to.equal( 'p' ); - expect( root.getChild( 0 ).maxOffset ).to.equal( 3 ); - expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 ); - expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' ); - expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foo' ); - - expect( root.getChild( 1 ).name ).to.equal( 'p' ); - expect( root.getChild( 1 ).maxOffset ).to.equal( 3 ); - expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 ); - expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' ); - expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'bar' ); - } ); - - it( 'should create an empty paragraph if we split at the end', () => { - doc.batch().split( new Position( root, [ 0, 6 ] ) ); - - expect( root.maxOffset ).to.equal( 2 ); - - expect( root.getChild( 0 ).name ).to.equal( 'p' ); - expect( root.getChild( 0 ).maxOffset ).to.equal( 6 ); - expect( count( root.getChild( 0 ).getAttributes() ) ).to.equal( 1 ); - expect( root.getChild( 0 ).getAttribute( 'key' ) ).to.equal( 'value' ); - expect( root.getChild( 0 ).getChild( 0 ).data ).to.equal( 'foobar' ); - - expect( root.getChild( 1 ).name ).to.equal( 'p' ); - expect( root.getChild( 1 ).maxOffset ).to.equal( 0 ); - expect( count( root.getChild( 1 ).getAttributes() ) ).to.equal( 1 ); - expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' ); - } ); - - it( 'should throw if we try to split a root', () => { - expect( () => { - doc.batch().split( new Position( root, [ 0 ] ) ); - } ).to.throw( CKEditorError, /^batch-split-root/ ); - } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - - const chain = batch.split( new Position( root, [ 0, 3 ] ) ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().split( new Position( root, [ 0, 3 ] ) ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'SplitDelta', () => { let splitDelta, doc, root; diff --git a/tests/model/delta/unwrapdelta.js b/tests/model/delta/unwrapdelta.js index 3fbacae2a..6cdeca743 100644 --- a/tests/model/delta/unwrapdelta.js +++ b/tests/model/delta/unwrapdelta.js @@ -4,10 +4,7 @@ */ import Document from '../../../src/model/document'; -import Element from '../../../src/model/element'; -import Text from '../../../src/model/text'; import Position from '../../../src/model/position'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import UnwrapDelta from '../../../src/model/delta/unwrapdelta'; import WrapDelta from '../../../src/model/delta/wrapdelta'; @@ -16,53 +13,6 @@ import MoveOperation from '../../../src/model/operation/moveoperation'; import RemoveOperation from '../../../src/model/operation/removeoperation'; import ReinsertOperation from '../../../src/model/operation/reinsertoperation'; -describe( 'Batch', () => { - let doc, root, p; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - p = new Element( 'p', [], new Text( 'xyz' ) ); - root.insertChildren( 0, [ new Text( 'a' ), p, new Text( 'b' ) ] ); - } ); - - describe( 'unwrap', () => { - it( 'should unwrap given element', () => { - doc.batch().unwrap( p ); - - expect( root.maxOffset ).to.equal( 5 ); - expect( root.getChild( 0 ).data ).to.equal( 'axyzb' ); - } ); - - it( 'should throw if element to unwrap has no parent', () => { - const element = new Element( 'p' ); - - expect( () => { - doc.batch().unwrap( element ); - } ).to.throw( CKEditorError, /^batch-unwrap-element-no-parent/ ); - } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - - const chain = batch.unwrap( p ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().unwrap( p ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'UnwrapDelta', () => { let unwrapDelta, doc, root; diff --git a/tests/model/delta/weakinsertdelta.js b/tests/model/delta/weakinsertdelta.js index 98117498a..813e3c64b 100644 --- a/tests/model/delta/weakinsertdelta.js +++ b/tests/model/delta/weakinsertdelta.js @@ -3,58 +3,8 @@ * For licensing, see LICENSE.md. */ -import Document from '../../../src/model/document'; -import Position from '../../../src/model/position'; -import Text from '../../../src/model/text'; import WeakInsertDelta from '../../../src/model/delta/weakinsertdelta'; -describe( 'Batch', () => { - let doc, root, batch, chain, attrs; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - root.insertChildren( 0, new Text( 'abc' ) ); - - batch = doc.batch(); - - attrs = [ [ 'bold', true ], [ 'foo', 'bar' ] ]; - - doc.selection.setAttributesTo( attrs ); - - chain = batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' ); - } ); - - describe( 'weakInsert', () => { - it( 'should insert given nodes at given position', () => { - expect( root.maxOffset ).to.equal( 6 ); - expect( root.getChild( 0 ).data ).to.equal( 'ab' ); - expect( root.getChild( 1 ).data ).to.equal( 'xyz' ); - expect( root.getChild( 2 ).data ).to.equal( 'c' ); - } ); - - it( 'should set inserted nodes attributes to same as current selection attributes', () => { - expect( Array.from( root.getChild( 1 ).getAttributes() ) ).to.deep.equal( attrs ); - } ); - - it( 'should be chainable', () => { - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'WeakInsertDelta', () => { it( 'should provide proper className', () => { expect( WeakInsertDelta.className ).to.equal( 'engine.model.delta.WeakInsertDelta' ); diff --git a/tests/model/delta/wrapdelta.js b/tests/model/delta/wrapdelta.js index 53688cffc..2315f692a 100644 --- a/tests/model/delta/wrapdelta.js +++ b/tests/model/delta/wrapdelta.js @@ -5,10 +5,7 @@ import Document from '../../../src/model/document'; import Position from '../../../src/model/position'; -import Range from '../../../src/model/range'; import Element from '../../../src/model/element'; -import Text from '../../../src/model/text'; -import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import WrapDelta from '../../../src/model/delta/wrapdelta'; import UnwrapDelta from '../../../src/model/delta/unwrapdelta'; @@ -17,86 +14,6 @@ import InsertOperation from '../../../src/model/operation/insertoperation'; import MoveOperation from '../../../src/model/operation/moveoperation'; import RemoveOperation from '../../../src/model/operation/removeoperation'; -describe( 'Batch', () => { - let doc, root, range; - - beforeEach( () => { - doc = new Document(); - root = doc.createRoot(); - - root.insertChildren( 0, new Text( 'foobar' ) ); - - range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 4 ] ) ); - } ); - - describe( 'wrap', () => { - it( 'should wrap flat range with given element', () => { - const p = new Element( 'p' ); - doc.batch().wrap( range, p ); - - expect( root.maxOffset ).to.equal( 5 ); - expect( root.getChild( 0 ).data ).to.equal( 'fo' ); - expect( root.getChild( 1 ) ).to.equal( p ); - expect( p.getChild( 0 ).data ).to.equal( 'ob' ); - expect( root.getChild( 2 ).data ).to.equal( 'ar' ); - } ); - - it( 'should wrap flat range with an element of given name', () => { - doc.batch().wrap( range, 'p' ); - - expect( root.maxOffset ).to.equal( 5 ); - expect( root.getChild( 0 ).data ).to.equal( 'fo' ); - expect( root.getChild( 1 ).name ).to.equal( 'p' ); - expect( root.getChild( 1 ).getChild( 0 ).data ).to.equal( 'ob' ); - expect( root.getChild( 2 ).data ).to.equal( 'ar' ); - } ); - - it( 'should throw if range to wrap is not flat', () => { - root.insertChildren( 1, [ new Element( 'p', [], new Text( 'xyz' ) ) ] ); - const notFlatRange = new Range( new Position( root, [ 3 ] ), new Position( root, [ 6, 2 ] ) ); - - expect( () => { - doc.batch().wrap( notFlatRange, 'p' ); - } ).to.throw( CKEditorError, /^batch-wrap-range-not-flat/ ); - } ); - - it( 'should throw if element to wrap with has children', () => { - const p = new Element( 'p', [], new Text( 'a' ) ); - - expect( () => { - doc.batch().wrap( range, p ); - } ).to.throw( CKEditorError, /^batch-wrap-element-not-empty/ ); - } ); - - it( 'should throw if element to wrap with has children', () => { - const p = new Element( 'p' ); - root.insertChildren( 0, p ); - - expect( () => { - doc.batch().wrap( range, p ); - } ).to.throw( CKEditorError, /^batch-wrap-element-attached/ ); - } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - - const chain = batch.wrap( range, 'p' ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().wrap( range, 'p' ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - } ); -} ); - describe( 'WrapDelta', () => { let wrapDelta, doc, root; From 4c0bd6e1cb0528a830440f7daa0b5f8c1076ee7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 20 Nov 2017 23:57:22 +0100 Subject: [PATCH 02/44] Refactored batch interface. --- src/model/batch.js | 140 ++++--- tests/model/batch.js | 853 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 784 insertions(+), 209 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index a871c5c2c..daaecd2fb 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -28,12 +28,11 @@ import RenameOperation from './operation/renameoperation'; import RootAttributeOperation from './operation/rootattributeoperation'; import DocumentFragment from './documentfragment'; +import Text from './text'; import Element from './element'; import Position from './position'; import Range from './range.js'; -import { normalizeNodes } from './writer'; - import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** @@ -129,34 +128,47 @@ export default class Batch { } } - /** - * Inserts a node or nodes at the given position. - * - * When inserted element is a {@link module:engine/model/documentfragment~DocumentFragment} and has markers its markers will - * be set to {@link module:engine/model/document~Document#markers}. - * - * @chainable - * @param {module:engine/model/position~Position} position Position of insertion. - * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. - */ - insert( position, nodes ) { - const normalizedNodes = normalizeNodes( nodes ); + createText( data, attributes = {} ) { + return new Text( data, attributes ); + } + + createElement( name, attributes ) { + return new Element( name, attributes ); + } + + createDocumentFragment() { + return new DocumentFragment(); + } + + insert( item, itemOrPosition, offset ) { + const position = Position.createAt( itemOrPosition, offset ); - // If nothing is inserted do not create delta and operation. - if ( normalizedNodes.length === 0 ) { - return this; + // For text that has no parent we need to make WeakInsert. + const delta = item instanceof Text && !item.parent ? new WeakInsertDelta() : new InsertDelta(); + + // If item is already in parent. + if ( item.parent ) { + // We need to check if item is going to be inserted to the same root. + if ( item.root === position.root ) { + // If it's we just need to move it. + return this.move( Range.createOn( item ), position ); + } + // If it isn't the same root + else { + // We need to remove this item from old position first. + this.remove( item ); + } } - const delta = new InsertDelta(); - const insert = new InsertOperation( position, normalizedNodes, this.document.version ); + 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 ( nodes instanceof DocumentFragment ) { - for ( const [ markerName, markerRange ] of nodes.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( @@ -171,40 +183,24 @@ export default class Batch { return this; } - /** - * Inserts a node or nodes at given position. {@link module:engine/model/batch~Batch#weakInsert weakInsert} is commonly used for actions - * like typing or plain-text paste (without formatting). There are two differences between - * {@link module:engine/model/batch~Batch#insert insert} and {@link module:engine/model/batch~Batch#weakInsert weakInsert}: - * - * * When using `weakInsert`, inserted nodes will have same attributes as the current attributes of - * {@link module:engine/model/document~Document#selection document selection}. - * * If {@link module:engine/model/operation/insertoperation~InsertOperation insert operation} position is inside a range changed by - * {@link module:engine/model/operation/attributeoperation~AttributeOperation attribute operation}, - * the attribute operation is split into two operations. - * Thanks to this, attribute change "omits" the inserted nodes. The correct behavior for `WeakInsertDelta` is that - * {@link module:engine/model/operation/attributeoperation~AttributeOperation AttributeOperation} does not "break" and also - * applies attributes for inserted nodes. This behavior has to be reflected during - * {@link module:engine/model/delta/transform~transform delta transformation}. - * - * @chainable - * @param {module:engine/model/position~Position} position Position of insertion. - * @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted. - */ - weakInsert( position, nodes ) { - const delta = new WeakInsertDelta(); - this.addDelta( delta ); + insertText( text, attributes, itemOrPosition, offset ) { + return this.insert( this.createText( text, attributes ), itemOrPosition, offset ); + } - nodes = normalizeNodes( nodes ); + insertElement( name, attributes, itemOrPosition, offset ) { + return this.insert( this.createElement( name, attributes ), itemOrPosition, offset ); + } - for ( const node of nodes ) { - node.setAttributesTo( this.document.selection.getAttributes() ); - } + append( item, parent ) { + return this.insert( item, parent, 'end' ); + } - const operation = new InsertOperation( position, nodes, this.document.version ); - delta.addOperation( operation ); - this.document.applyOperation( operation ); + appendText( text, attributes, parent ) { + return this.insert( this.createText( text, attributes ), parent, 'end' ); + } - return this; + appendElement( text, attributes, parent ) { + return this.insert( this.createElement( text, attributes ), parent, 'end' ); } /** @@ -309,34 +305,25 @@ export default class Batch { * Moves given {@link module:engine/model/item~Item model item} or given range to target position. * * @chainable - * @method module:engine/model/batch~Batch#move - * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range of nodes to move. - * @param {module:engine/model/position~Position} targetPosition Position where moved nodes will be inserted. */ - move( itemOrRange, targetPosition ) { - const delta = new MoveDelta(); - this.addDelta( delta ); + move( range, itemOrPosition, offset ) { + 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 addOperation = ( sourcePosition, howMany, targetPosition ) => { - const operation = new MoveOperation( sourcePosition, howMany, targetPosition, this.document.version ); - delta.addOperation( operation ); - this.document.applyOperation( operation ); - }; + const position = Position.createAt( itemOrPosition, offset ); - if ( itemOrRange instanceof Range ) { - if ( !itemOrRange.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 delta = new MoveDelta(); + this.addDelta( delta ); - addOperation( itemOrRange.start, itemOrRange.end.offset - itemOrRange.start.offset, targetPosition ); - } else { - addOperation( Position.createBefore( itemOrRange ), 1, targetPosition ); - } + const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, this.document.version ); + delta.addOperation( operation ); + this.document.applyOperation( operation ); return this; } @@ -704,9 +691,8 @@ function setAttributeToItem( batch, key, value, item ) { const previousValue = item.getAttribute( key ); let range, operation; - const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta(); - if ( previousValue != value ) { + const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta(); batch.addDelta( delta ); if ( item.is( 'rootElement' ) ) { diff --git a/tests/model/batch.js b/tests/model/batch.js index 207d7428a..094448e4d 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -5,6 +5,8 @@ import Batch from '../../src/model/batch'; import Delta from '../../src/model/delta/delta'; +import InsertDelta from '../../src/model/delta/insertdelta'; +import WeakInsertDelta from '../../src/model/delta/weakinsertdelta'; import Operation from '../../src/model/operation/operation'; import InsertOperation from '../../src/model/operation/insertoperation'; @@ -93,57 +95,270 @@ describe( 'Batch', () => { } ); } ); - describe( 'insert', () => { - let doc, root, batch, p, ul, chain; + describe( 'createText()', () => { + let doc, batch; beforeEach( () => { doc = new Document(); - root = doc.createRoot(); - root.insertChildren( 0, new Text( 'abc' ) ); + batch = doc.batch(); + } ); + + it( 'should create text node', () => { + const text = batch.createText( 'foo' ); + + expect( text ).to.instanceof( Text ); + expect( text.data ).to.equal( 'foo' ); + expect( Array.from( text.getAttributes() ) ).to.length( 0 ); + } ); + + it( 'should create text with attributes', () => { + const text = batch.createText( 'foo', { foo: 'bar', biz: 'baz' } ); + + expect( Array.from( text.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] ); + } ); + } ); + + describe( 'createElement()', () => { + let doc, batch; + beforeEach( () => { + doc = new Document(); batch = doc.batch(); + } ); - p = new Element( 'p' ); - ul = new Element( 'ul' ); + it( 'should create element', () => { + const element = batch.createElement( 'foo' ); - chain = batch.insert( new Position( root, [ 2 ] ), [ p, ul ] ); + expect( element ).to.instanceof( Element ); + expect( element.name ).to.equal( 'foo' ); + expect( Array.from( element.getAttributes() ) ).to.length( 0 ); } ); - it( 'should insert given nodes at given position', () => { - expect( root.childCount ).to.equal( 4 ); - expect( root.maxOffset ).to.equal( 5 ); - expect( root.getChild( 1 ) ).to.equal( p ); - expect( root.getChild( 2 ) ).to.equal( ul ); + it( 'should create element with attributes', () => { + const element = batch.createText( 'foo', { foo: 'bar', biz: 'baz' } ); + + expect( Array.from( element.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'biz', 'baz' ] ] ); } ); + } ); - it( 'should be chainable', () => { - expect( chain ).to.equal( batch ); + describe( 'createDocumentFragment()', () => { + let doc, batch; + + beforeEach( () => { + doc = new Document(); + batch = doc.batch(); } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.insert( new Position( root, [ 2 ] ), [ p, ul ] ); + it( 'should create element', () => { + const element = batch.createDocumentFragment(); - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); + expect( element ).to.instanceof( DocumentFragment ); + } ); + } ); - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + describe( 'insert()', () => { + let doc, batch; + + beforeEach( () => { + doc = new Document(); + batch = doc.batch(); } ); - it( 'should transfer markers from given DocumentFragment', () => { - const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] ); + it( 'should insert node at given position', () => { + const parent = batch.createDocumentFragment(); + const child = batch.createElement( 'child' ); + const textChild = batch.createText( 'textChild' ); + + batch.insert( child, new Position( parent, [ 0 ] ) ); + batch.insert( textChild, new Position( parent, [ 1 ] ) ); + + expect( Array.from( parent ) ).to.deep.equal( [ child, textChild ] ); + } ); + + it( 'should insert node at the beginning of given element', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child' ); + const child2 = batch.createElement( 'child' ); + + batch.insert( child1, parent ); + batch.insert( child2, parent ); + + expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child2, child1 ] ); + } ); + + it( 'should insert node at the end of given element', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child' ); + const child2 = batch.createElement( 'child' ); + + batch.insert( child1, parent ); + batch.insert( child2, parent, 'end' ); + + expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2 ] ); + } ); + + it( 'should insert node at the given offset of given element', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child' ); + const child2 = batch.createElement( 'child' ); + const child3 = batch.createElement( 'child' ); + + batch.insert( child3, parent ); + batch.insert( child1, parent ); + batch.insert( child2, parent, 1 ); + + expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] ); + } ); + + it( 'should insert node before the given node', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child' ); + const child2 = batch.createElement( 'child' ); + const child3 = batch.createElement( 'child' ); + + batch.insert( child3, parent ); + batch.insert( child1, parent ); + batch.insert( child2, child3, 'before' ); + + expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] ); + } ); + + it( 'should insert node after the given node', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child' ); + const child2 = batch.createElement( 'child' ); + const child3 = batch.createElement( 'child' ); + + batch.insert( child3, parent ); + batch.insert( child1, parent ); + batch.insert( child2, child1, 'after' ); + + expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] ); + } ); + + it( 'should move element from one parent to the other within different root', () => { + const parent1 = batch.createDocumentFragment(); + const parent2 = batch.createDocumentFragment(); + const b = batch.createText( 'b', { foo: 'bar' } ); + + batch + .insert( batch.createText( 'a' ), parent1 ) + .insert( b, parent1, 'end' ) + .insert( batch.createText( 'c' ), parent1, 'end' ); + + batch.insert( batch.createText( 'ac' ), parent2 ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insert( b, parent2, 1 ); + + // Verify result. + expect( Array.from( parent1, item => item.data ) ).to.deep.equal( [ 'ac' ] ); + expect( Array.from( parent2, item => item.data ) ).to.deep.equal( [ 'a', 'b', 'c' ] ); + + // Verify deltas and operations. + sinon.assert.calledTwice( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should move element from one parent to the other within the same root', () => { + const root = batch.createDocumentFragment(); + const parent1 = batch.createElement( 'parent' ); + const parent2 = batch.createElement( 'parent' ); + const b = batch.createText( 'b', { foo: 'bar' } ); + + batch + .insert( parent1, root ) + .insert( batch.createText( 'a' ), parent1 ) + .insert( b, parent1, 'end' ) + .insert( batch.createText( 'c' ), parent1, 'end' ); + + batch + .insert( parent2, root ) + .insert( batch.createText( 'ac' ), parent2 ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insert( b, parent2, 1 ); + + // Verify result. + expect( Array.from( parent1.getChildren(), item => item.data ) ).to.deep.equal( [ 'ac' ] ); + expect( Array.from( parent2.getChildren(), item => item.data ) ).to.deep.equal( [ 'a', 'b', 'c' ] ); + + // Verify deltas and operations. + sinon.assert.calledOnce( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should create proper delta for element', () => { + const parent = batch.createDocumentFragment(); + const element = batch.createElement( 'child' ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insert( element, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); + expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should create proper delta for text with no parent', () => { + const parent = batch.createDocumentFragment(); + const text = batch.createText( 'child' ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insert( text, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should create proper delta for text with parent', () => { + const parent1 = batch.createDocumentFragment(); + const parent2 = batch.createDocumentFragment(); + const text = batch.createText( 'child' ); + + batch.insert( text, parent1 ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insert( text, parent2 ); + + sinon.assert.calledTwice( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); + expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it.skip( 'should transfer markers from given DocumentFragment', () => { + const documentFragment = batch.createDocumentFragment(); + const li = batch.createElement( 'li' ); + + batch.insert( batch.createText( 'foo bar' ), li ); + batch.insert( li, documentFragment ); + const marker = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 5 ] ) ); documentFragment.markers.set( 'marker', marker ); - batch.insert( new Position( root, [ 3, 0 ] ), documentFragment ); + batch.insert( documentFragment, new Position( root, [ 3, 0 ] ) ); expect( Array.from( doc.markers ).length ).to.equal( 1 ); expect( stringify( root, doc.markers.get( 'marker' ).getRange() ) ).to.equal( 'ab

c' ); } ); - it( 'should set each marker as separate operation', () => { + it.skip( 'should set each marker as separate operation', () => { sinon.spy( doc, 'applyOperation' ); const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] ); @@ -161,65 +376,447 @@ describe( 'Batch', () => { expect( doc.applyOperation.thirdCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); } ); - it( 'should not create a delta and an operation if no nodes were inserted', () => { - sinon.spy( doc, 'applyOperation' ); + it( 'should be chainable', () => { + const parent = batch.createDocumentFragment(); + const child = batch.createElement( 'child' ); + + expect( batch.insert( child, parent ) ).to.equal( batch ); + } ); + } ); + + describe( 'insertText()', () => { + let doc, batch; + beforeEach( () => { + doc = new Document(); batch = doc.batch(); + } ); - batch.insert( new Position( root, [ 0 ] ), [] ); + it( 'should create and insert text node with attributes at given position', () => { + const parent = batch.createDocumentFragment(); - expect( batch.deltas.length ).to.equal( 0 ); - expect( doc.applyOperation.called ).to.be.false; + batch.insertText( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Text ); + expect( parent.getChild( 0 ).data ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] ); + } ); + + it( 'should create and insert text node with no attributes at given position', () => { + const parent = batch.createDocumentFragment(); + + batch.insertText( 'foo', null, new Position( parent, [ 0 ] ) ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Text ); + expect( parent.getChild( 0 ).data ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); + } ); + + it( 'should create and insert text node at the beginning of given element', () => { + const parent = batch.createDocumentFragment(); + + batch.insert( batch.createElement( 'child' ), parent ); + + batch.insertText( 'foo', null, parent ); + + expect( parent.childCount ).to.equal( 2 ); + expect( parent.getChild( 0 ) ).to.instanceof( Text ); + expect( parent.getChild( 1 ) ).to.instanceof( Element ); + } ); + + it( 'should create and insert text node at the end of given element', () => { + const parent = batch.createDocumentFragment(); + + batch.insert( batch.createElement( 'child' ), parent ); + + batch.insertText( 'foo', null, parent, 'end' ); + + expect( parent.childCount ).to.equal( 2 ); + expect( parent.getChild( 0 ) ).to.instanceof( Element ); + expect( parent.getChild( 1 ) ).to.instanceof( Text ); + } ); + + it( 'should create and insert text node at the given offset of given element', () => { + const parent = batch.createDocumentFragment(); + + batch.insert( batch.createElement( 'child' ), parent ); + batch.insert( batch.createElement( 'child' ), parent ); + + batch.insertText( 'foo', null, parent, 1 ); + + expect( parent.childCount ).to.equal( 3 ); + expect( parent.getChild( 0 ) ).to.instanceof( Element ); + expect( parent.getChild( 1 ) ).to.instanceof( Text ); + expect( parent.getChild( 2 ) ).to.instanceof( Element ); + } ); + + it( 'should create and insert text node before the given node', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child' ); + const child2 = batch.createElement( 'child' ); + + batch.insert( child1, parent ); + batch.insert( child2, parent, 'end' ); + + batch.insertText( 'foo', null, child2, 'before' ); + + expect( parent.childCount ).to.equal( 3 ); + expect( parent.getChild( 0 ) ).to.instanceof( Element ); + expect( parent.getChild( 1 ) ).to.instanceof( Text ); + expect( parent.getChild( 2 ) ).to.instanceof( Element ); + } ); + + it( 'should create and insert text node after the given node', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child' ); + const child2 = batch.createElement( 'child' ); + + batch.insert( child1, parent ); + batch.insert( child2, parent, 'end' ); + + batch.insertText( 'foo', null, child1, 'after' ); + + expect( parent.childCount ).to.equal( 3 ); + expect( parent.getChild( 0 ) ).to.instanceof( Element ); + expect( parent.getChild( 1 ) ).to.instanceof( Text ); + expect( parent.getChild( 2 ) ).to.instanceof( Element ); + } ); + + it( 'should create proper delta', () => { + const parent = batch.createDocumentFragment(); + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insertText( 'foo', null, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should be chainable', () => { + const parent = batch.createDocumentFragment(); + + expect( batch.insertText( 'foo', null, parent ) ).to.equal( batch ); } ); } ); - describe( 'weakInsert()', () => { - let doc, root, batch, chain, attrs; + describe( 'insertElement()', () => { + let doc, batch; beforeEach( () => { doc = new Document(); - root = doc.createRoot(); + batch = doc.batch(); + } ); + + it( 'should create and insert element with attributes at given position', () => { + const parent = batch.createDocumentFragment(); + + batch.insertElement( 'foo', { bar: 'biz' }, new Position( parent, [ 0 ] ) ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Element ); + expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] ); + } ); + + it( 'should create and insert element with no attributes at given position', () => { + const parent = batch.createDocumentFragment(); + + batch.insertElement( 'foo', null, new Position( parent, [ 0 ] ) ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Element ); + expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); + } ); + + it( 'should create and insert element at the beginning of given element', () => { + const parent = batch.createDocumentFragment(); + + batch.insert( batch.createElement( 'child' ), parent ); + + batch.insertElement( 'foo', null, parent ); + + expect( parent.childCount ).to.equal( 2 ); + expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); + expect( parent.getChild( 1 ).name ).to.equal( 'child' ); + } ); + + it( 'should create and insert element at the end of given element', () => { + const parent = batch.createDocumentFragment(); + + batch.insert( batch.createElement( 'child' ), parent ); + + batch.insertElement( 'foo', null, parent, 'end' ); + + expect( parent.childCount ).to.equal( 2 ); + expect( parent.getChild( 0 ).name ).to.equal( 'child' ); + expect( parent.getChild( 1 ).name ).to.equal( 'foo' ); + } ); + + it( 'should create and insert element at the given offset of given element', () => { + const parent = batch.createDocumentFragment(); + + batch.insert( batch.createElement( 'child1' ), parent ); + batch.insert( batch.createElement( 'child2' ), parent, 'end' ); + + batch.insertElement( 'foo', null, parent, 1 ); + + expect( parent.childCount ).to.equal( 3 ); + expect( parent.getChild( 0 ).name ).to.equal( 'child1' ); + expect( parent.getChild( 1 ).name ).to.equal( 'foo' ); + expect( parent.getChild( 2 ).name ).to.equal( 'child2' ); + } ); + + it( 'should create and insert element before the given node', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child1' ); + const child2 = batch.createElement( 'child2' ); + + batch.insert( child1, parent ); + batch.insert( child2, parent, 'end' ); + + batch.insertElement( 'foo', null, child2, 'before' ); + + expect( parent.childCount ).to.equal( 3 ); + expect( parent.getChild( 0 ).name ).to.equal( 'child1' ); + expect( parent.getChild( 1 ).name ).to.equal( 'foo' ); + expect( parent.getChild( 2 ).name ).to.equal( 'child2' ); + } ); + + it( 'should create and insert element after the given node', () => { + const parent = batch.createDocumentFragment(); + const child1 = batch.createElement( 'child1' ); + const child2 = batch.createElement( 'child2' ); + + batch.insert( child1, parent ); + batch.insert( child2, parent, 'end' ); + + batch.insertElement( 'foo', null, child1, 'after' ); + + expect( parent.childCount ).to.equal( 3 ); + expect( parent.getChild( 0 ).name ).to.equal( 'child1' ); + expect( parent.getChild( 1 ).name ).to.equal( 'foo' ); + expect( parent.getChild( 2 ).name ).to.equal( 'child2' ); + } ); + + it( 'should create proper delta', () => { + const parent = batch.createDocumentFragment(); + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insertText( 'foo', null, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should be chainable', () => { + const parent = batch.createDocumentFragment(); + + expect( batch.insertElement( 'foo', null, parent ) ).to.equal( batch ); + } ); + } ); + + describe( 'append()', () => { + let doc, batch; + + beforeEach( () => { + doc = new Document(); + batch = doc.batch(); + } ); + + it( 'should insert element at the end of the parent', () => { + const parent = doc.batch().createDocumentFragment(); + const childText = doc.batch().createText( 'foo' ); + const childElement = doc.batch().createElement( 'foo' ); + + batch.append( childText, parent ); + batch.append( childElement, parent ); + + expect( Array.from( parent ) ).to.deep.equal( [ childText, childElement ] ); + } ); + + it( 'should create proper delta', () => { + const parent = batch.createDocumentFragment(); + const text = batch.createText( 'foo' ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.append( text, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should move element from one parent to the other within different root', () => { + const parent1 = batch.createDocumentFragment(); + const parent2 = batch.createDocumentFragment(); + const b = batch.createText( 'b', { foo: 'bar' } ); + + batch + .append( batch.createText( 'a' ), parent1 ) + .append( b, parent1 ) + .append( batch.createText( 'c' ), parent1 ); + + const spy = sinon.spy( doc, 'applyOperation' ); - root.insertChildren( 0, new Text( 'abc' ) ); + batch.append( b, parent2, 1 ); + // Verify result. + expect( Array.from( parent1, item => item.data ) ).to.deep.equal( [ 'ac' ] ); + expect( Array.from( parent2, item => item.data ) ).to.deep.equal( [ 'b' ] ); + + // Verify deltas and operations. + sinon.assert.calledTwice( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should move element from one parent to the other within the same root', () => { + const root = batch.createDocumentFragment(); + const parent1 = batch.createElement( 'parent' ); + const parent2 = batch.createElement( 'parent' ); + const b = batch.createText( 'b', { foo: 'bar' } ); + + batch + .append( parent1, root ) + .append( batch.createText( 'a' ), parent1 ) + .append( b, parent1 ) + .append( batch.createText( 'c' ), parent1 ); + + batch.append( parent2, root ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.append( b, parent2 ); + + // Verify result. + expect( Array.from( parent1.getChildren(), item => item.data ) ).to.deep.equal( [ 'ac' ] ); + expect( Array.from( parent2.getChildren(), item => item.data ) ).to.deep.equal( [ 'b' ] ); + + // Verify deltas and operations. + sinon.assert.calledOnce( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should be chainable', () => { + expect( batch.append( batch.createElement( 'a' ), batch.createElement( 'b' ) ) ).to.equal( batch ); + } ); + } ); + + describe( 'appendText()', () => { + let doc, batch; + + beforeEach( () => { + doc = new Document(); batch = doc.batch(); + } ); - attrs = [ [ 'bold', true ], [ 'foo', 'bar' ] ]; + it( 'should create and insert text node with attributes at the end of the parent', () => { + const parent = batch.createDocumentFragment(); - doc.selection.setAttributesTo( attrs ); + batch.appendText( 'foo', { bar: 'biz' }, parent ); + batch.appendText( 'bar', { biz: 'bar' }, parent ); - chain = batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' ); + expect( parent.childCount ).to.equal( 2 ); + expect( parent.getChild( 0 ).data ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] ); + expect( parent.getChild( 1 ).data ).to.equal( 'bar' ); + expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] ); } ); - it( 'should insert given nodes at given position', () => { - expect( root.maxOffset ).to.equal( 6 ); - expect( root.getChild( 0 ).data ).to.equal( 'ab' ); - expect( root.getChild( 1 ).data ).to.equal( 'xyz' ); - expect( root.getChild( 2 ).data ).to.equal( 'c' ); + it( 'should create and insert text node with no attributes at the end of the parent', () => { + const parent = batch.createDocumentFragment(); + + batch.appendText( 'foo', null, parent ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Text ); + expect( parent.getChild( 0 ).data ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); } ); - it( 'should set inserted nodes attributes to same as current selection attributes', () => { - expect( Array.from( root.getChild( 1 ).getAttributes() ) ).to.deep.equal( attrs ); + it( 'should create proper delta', () => { + const parent = batch.createDocumentFragment(); + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.appendText( 'foo', null, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); it( 'should be chainable', () => { - expect( chain ).to.equal( batch ); + const parent = batch.createDocumentFragment(); + + expect( batch.appendText( 'foo', null, parent ) ).to.equal( batch ); } ); + } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.weakInsert( new Position( root, [ 2 ] ), 'xyz' ); + describe( 'appendElement()', () => { + let doc, batch; - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); + beforeEach( () => { + doc = new Document(); + batch = doc.batch(); + } ); - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + it( 'should create and insert element with attributes at the end of the parent', () => { + const parent = batch.createDocumentFragment(); + + batch.appendElement( 'foo', { bar: 'biz' }, parent ); + batch.appendElement( 'bar', { biz: 'bar' }, parent ); + + expect( parent.childCount ).to.equal( 2 ); + expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'bar', 'biz' ] ] ); + expect( parent.getChild( 1 ).name ).to.equal( 'bar' ); + expect( Array.from( parent.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'biz', 'bar' ] ] ); + } ); + + it( 'should create and insert element with no attributes at the end of the parent', () => { + const parent = batch.createDocumentFragment(); + + batch.appendElement( 'foo', null, parent ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); + } ); + + it( 'should create proper delta', () => { + const parent = batch.createDocumentFragment(); + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.appendElement( 'foo', null, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); + expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should be chainable', () => { + const parent = batch.createDocumentFragment(); + + expect( batch.appendElement( 'foo', null, parent ) ).to.equal( batch ); } ); } ); describe( 'setAttribute() / removeAttribute()', () => { - let batch, doc, root; + let batch, doc, root, spy; const correctDeltaMatcher = sinon.match( operation => { return operation.delta && operation.delta.batch && operation.delta.batch == batch; @@ -231,54 +828,47 @@ describe( 'Batch', () => { batch = doc.batch(); } ); - function getOperationsCount() { - let totalNumber = 0; - - for ( const delta of batch.deltas ) { - totalNumber += count( delta.operations ); - } - - return totalNumber; - } - describe( 'change attribute on node', () => { let node, text; beforeEach( () => { - node = new Element( 'p', { a: 1 } ); - text = new Text( 'c', { a: 1 } ); + node = batch.createElement( 'p', { a: 1 } ); + text = batch.createText( 'c', { a: 1 } ); + + batch.append( node, root ); + batch.append( text, root ); - root.insertChildren( 0, [ node, text ] ); + spy = sinon.spy( doc, 'applyOperation' ); } ); describe( 'setAttribute', () => { it( 'should create the attribute on element', () => { batch.setAttribute( node, 'b', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( node.getAttribute( 'b' ) ).to.equal( 2 ); } ); it( 'should change the attribute of element', () => { batch.setAttribute( node, 'a', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( node.getAttribute( 'a' ) ).to.equal( 2 ); } ); it( 'should create the attribute on text node', () => { batch.setAttribute( text, 'b', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 ); } ); it( 'should change the attribute of text node', () => { batch.setAttribute( text, 'a', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 ); } ); it( 'should do nothing if the attribute value is the same', () => { batch.setAttribute( node, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); expect( node.getAttribute( 'a' ) ).to.equal( 1 ); } ); @@ -288,29 +878,28 @@ describe( 'Batch', () => { } ); it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); batch.setAttribute( node, 'b', 2 ); - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + sinon.assert.calledWith( spy, correctDeltaMatcher ); } ); } ); describe( 'removeAttribute', () => { it( 'should remove the attribute from element', () => { batch.removeAttribute( node, 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( node.getAttribute( 'a' ) ).to.be.undefined; } ); it( 'should remove the attribute from character', () => { batch.removeAttribute( text, 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined; } ); it( 'should do nothing if the attribute is not set', () => { batch.removeAttribute( node, 'b' ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); } ); it( 'should be chainable', () => { @@ -319,26 +908,29 @@ describe( 'Batch', () => { } ); it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); batch.removeAttribute( node, 'a' ); - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + sinon.assert.calledWith( spy, correctDeltaMatcher ); } ); } ); } ); describe( 'change attribute on range', () => { beforeEach( () => { - root.insertChildren( 0, [ - new Text( 'xxx', { a: 1 } ), - new Text( 'xxx' ), - new Text( 'xxx', { a: 1 } ), - new Text( 'xxx', { a: 2 } ), - new Text( 'xxx' ), - new Text( 'xxx', { a: 1 } ), - new Element( 'e', { a: 2 }, new Text( 'xxx' ) ), - new Text( 'xxx' ) - ] ); + const element = batch.createElement( 'e', { a: 2 } ); + + batch + .appendText( 'xxx', { a: 1 }, root ) + .appendText( 'xxx', null, root ) + .appendText( 'xxx', { a: 1 }, root ) + .appendText( 'xxx', { a: 2 }, root ) + .appendText( 'xxx', null, root ) + .appendText( 'xxx', { a: 1 }, root ) + .appendText( 'xxx', null, element ) + .append( element, root ) + .appendText( 'xxx', null, root ); + + spy = sinon.spy( doc, 'applyOperation' ); } ); function getRange( startIndex, endIndex ) { @@ -353,7 +945,9 @@ describe( 'Batch', () => { for ( const delta of batch.deltas ) { for ( const operation of delta.operations ) { - totalNumber += count( operation.range.getItems( { singleCharacters: true } ) ); + if ( operation.range ) { + totalNumber += count( operation.range.getItems( { singleCharacters: true } ) ); + } } } @@ -372,42 +966,42 @@ describe( 'Batch', () => { describe( 'setAttribute', () => { it( 'should set the attribute on the range', () => { batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 3 ); expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' ); } ); it( 'should split the operations if parts of the range have different attributes', () => { batch.setAttribute( getRange( 4, 14 ), 'a', 3 ); - expect( getOperationsCount() ).to.equal( 4 ); + expect( spy.callCount ).to.equal( 4 ); expect( getChangesAttrsCount() ).to.equal( 10 ); expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' ); } ); it( 'should split the operations if parts of the part of the range have the attribute', () => { batch.setAttribute( getRange( 4, 14 ), 'a', 2 ); - expect( getOperationsCount() ).to.equal( 3 ); + expect( spy.callCount ).to.equal( 3 ); expect( getChangesAttrsCount() ).to.equal( 7 ); expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' ); } ); it( 'should strip the range if the beginning have the attribute', () => { batch.setAttribute( getRange( 1, 5 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' ); } ); it( 'should strip the range if the ending have the attribute', () => { batch.setAttribute( getRange( 13, 17 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' ); } ); it( 'should do nothing if the range has attribute', () => { batch.setAttribute( getRange( 0, 3 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); @@ -418,7 +1012,7 @@ describe( 'Batch', () => { ); batch.setAttribute( range, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' ); } ); @@ -430,7 +1024,7 @@ describe( 'Batch', () => { ); batch.setAttribute( range, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 4 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' ); } ); @@ -442,19 +1036,19 @@ describe( 'Batch', () => { ); batch.setAttribute( range, 'a', 3 ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should not create an operation if is collapsed', () => { batch.setAttribute( getRange( 3, 3 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should create a proper operations for the mixed range', () => { batch.setAttribute( getRange( 0, 20 ), 'a', 1 ); - expect( getOperationsCount() ).to.equal( 5 ); + expect( spy.callCount ).to.equal( 5 ); expect( getChangesAttrsCount() ).to.equal( 14 ); expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' ); } ); @@ -465,7 +1059,6 @@ describe( 'Batch', () => { } ); it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; @@ -475,42 +1068,42 @@ describe( 'Batch', () => { describe( 'removeAttribute', () => { it( 'should remove the attribute on the range', () => { batch.removeAttribute( getRange( 0, 2 ), 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' ); } ); it( 'should split the operations if parts of the range have different attributes', () => { batch.removeAttribute( getRange( 7, 11 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); + expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 4 ); expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' ); } ); it( 'should split the operations if parts of the part of the range have no attribute', () => { batch.removeAttribute( getRange( 1, 7 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); + expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 3 ); expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' ); } ); it( 'should strip the range if the beginning have no attribute', () => { batch.removeAttribute( getRange( 4, 12 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); + expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 6 ); expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); } ); it( 'should strip the range if the ending have no attribute', () => { batch.removeAttribute( getRange( 7, 15 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); + expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 5 ); expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' ); } ); it( 'should do nothing if the range has no attribute', () => { batch.removeAttribute( getRange( 4, 5 ), 'a' ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); @@ -521,27 +1114,27 @@ describe( 'Batch', () => { ); batch.removeAttribute( range, 'a' ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); expect( getChangesAttrsCount() ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should not apply operation twice in the range contains opening and closing tags', () => { batch.removeAttribute( getRange( 18, 22 ), 'a' ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 1 ); expect( getCompressedAttrs() ).to.equal( '111---111222---111-------' ); } ); it( 'should not create an operation if range is collapsed', () => { batch.removeAttribute( getRange( 3, 3 ), 'a' ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should create a proper operations for the mixed range', () => { batch.removeAttribute( getRange( 3, 15 ), 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); + expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 6 ); expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); } ); @@ -552,33 +1145,35 @@ describe( 'Batch', () => { } ); it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); batch.removeAttribute( getRange( 0, 2 ), 'a' ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + sinon.assert.calledWith( spy, correctDeltaMatcher ); } ); } ); } ); describe( 'change attribute on root element', () => { + beforeEach( () => { + spy = sinon.spy( doc, 'applyOperation' ); + } ); + describe( 'setAttribute', () => { it( 'should create the attribute on root', () => { batch.setAttribute( root, 'b', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'b' ) ).to.equal( 2 ); } ); it( 'should change the attribute of root', () => { batch.setAttribute( root, 'a', 2 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'a' ) ).to.equal( 2 ); } ); it( 'should do nothing if the attribute value is the same', () => { batch.setAttribute( root, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); batch.setAttribute( root, 'a', 1 ); - expect( getOperationsCount() ).to.equal( 1 ); + expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'a' ) ).to.equal( 1 ); } ); } ); @@ -587,13 +1182,14 @@ describe( 'Batch', () => { it( 'should remove the attribute from root', () => { batch.setAttribute( root, 'a', 1 ); batch.removeAttribute( root, 'a' ); - expect( getOperationsCount() ).to.equal( 2 ); + + expect( spy.callCount ).to.equal( 2 ); expect( root.getAttribute( 'a' ) ).to.be.undefined; } ); it( 'should do nothing if the attribute is not set', () => { batch.removeAttribute( root, 'b' ); - expect( getOperationsCount() ).to.equal( 0 ); + expect( spy.callCount ).to.equal( 0 ); } ); } ); } ); @@ -669,7 +1265,7 @@ describe( 'Batch', () => { } ); describe( 'move()', () => { - let doc, root, div, p, batch, chain; + let doc, root, range, div, p, batch, chain; beforeEach( () => { doc = new Document(); @@ -683,19 +1279,12 @@ describe( 'Batch', () => { root.insertChildren( 0, [ div, p ] ); - batch = doc.batch(); - } ); - - it( 'should move specified node', () => { - batch.move( div, new Position( root, [ 2 ] ) ); + range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) ); - expect( root.maxOffset ).to.equal( 2 ); - expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); - expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'PggggPfoobarPhhhhP' ); + batch = doc.batch(); } ); it( 'should move flat range of nodes', () => { - const range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 7 ] ) ); batch.move( range, new Position( root, [ 1, 3 ] ) ); expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggggPfoPhhhhP' ); @@ -711,14 +1300,14 @@ describe( 'Batch', () => { } ); it( 'should be chainable', () => { - chain = batch.move( div, new Position( root, [ 1, 3 ] ) ); + chain = batch.move( range, new Position( root, [ 1, 3 ] ) ); expect( chain ).to.equal( batch ); } ); it( 'should add delta to batch and operation to delta before applying operation', () => { sinon.spy( doc, 'applyOperation' ); - batch.move( div, new Position( root, [ 2 ] ) ); + batch.move( range, new Position( root, [ 2 ] ) ); const correctDeltaMatcher = sinon.match( operation => { return operation.delta && operation.delta.batch && operation.delta.batch == batch; From 8da94737b9c1a27da2c3b14f8c362fcbb9ad34e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 21 Nov 2017 00:02:46 +0100 Subject: [PATCH 03/44] Improved checking if target element is a root. --- src/model/documentselection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/documentselection.js b/src/model/documentselection.js index 7db3fb86b..8b6375a4e 100644 --- a/src/model/documentselection.js +++ b/src/model/documentselection.js @@ -723,7 +723,7 @@ function clearAttributesStoredInElement( changes, batch, document ) { // `changes.range` is not set in case of rename, root and marker operations. // None of them may lead to the element becoming non-empty. - if ( !changeParent || changeParent.isEmpty ) { + if ( !changeParent || changeParent.is( 'documentFragment' ) || changeParent.isEmpty ) { return; } From f106f8d824d48378f4e50d3d8a7cd2784636a2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 21 Nov 2017 08:59:10 +0100 Subject: [PATCH 04/44] Added more methods for changing attributes to the Batch interface. --- src/model/batch.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/model/batch.js b/src/model/batch.js index daaecd2fb..63ab68f08 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -223,6 +223,12 @@ export default class Batch { return this; } + setAttributes( itemOrRange, attributes ) { + for ( const attribute of Object.keys( attributes ) ) { + this.setAttribute( itemOrRange, attribute, attributes[ attribute ] ); + } + } + /** * 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}. @@ -243,6 +249,22 @@ export default class Batch { return this; } + clearAttributes( itemOrRange ) { + const removeAttributesFromItem = item => { + for ( const attribute of item.getAttributeKeys() ) { + this.removeAttribute( item, attribute ); + } + }; + + if ( !( itemOrRange instanceof Range ) ) { + removeAttributesFromItem( itemOrRange ); + } else { + for ( const item of itemOrRange.getItems() ) { + removeAttributesFromItem( item ); + } + } + } + /** * Merges two siblings at the given position. * From 6f5ea7fd5ef2077a59bd51f73a83cff0daf4319a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 21 Nov 2017 21:06:51 +0100 Subject: [PATCH 05/44] Improved batch API. --- src/model/batch.js | 155 ++++++++++-------- tests/model/batch.js | 367 +++++++++++++++++++++++++++++++++---------- 2 files changed, 369 insertions(+), 153 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index 63ab68f08..afcc99d2d 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -30,9 +30,12 @@ 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'; /** @@ -143,17 +146,17 @@ export default class Batch { insert( item, itemOrPosition, offset ) { const position = Position.createAt( itemOrPosition, offset ); - // For text that has no parent we need to make WeakInsert. + // 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 is already in parent. + // If item has a parent already. if ( item.parent ) { - // We need to check if item is going to be inserted to the same root. - if ( item.root === position.root ) { + // We need to check if item is going to be inserted within the same document. + if ( isTheSameDocument( item.root, position.root ) ) { // If it's we just need to move it. return this.move( Range.createOn( item ), position ); } - // If it isn't the same root + // If it isn't the same root. else { // We need to remove this item from old position first. this.remove( item ); @@ -224,8 +227,8 @@ export default class Batch { } setAttributes( itemOrRange, attributes ) { - for ( const attribute of Object.keys( attributes ) ) { - this.setAttribute( itemOrRange, attribute, attributes[ attribute ] ); + for ( const [ key, val ] of toMap( attributes ) ) { + this.setAttribute( itemOrRange, key, val ); } } @@ -265,64 +268,6 @@ export default class Batch { } } - /** - * 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. - * - * @chainable - * @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 ); - - return this; - } - /** * Moves given {@link module:engine/model/item~Item model item} or given range to target position. * @@ -340,6 +285,16 @@ export default class Batch { const position = Position.createAt( itemOrPosition, offset ); + if ( !isTheSameDocument( 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 ); @@ -377,12 +332,72 @@ export default class Batch { addRemoveDelta( flat.start, flat.end.offset - flat.start.offset ); } } else { - addRemoveDelta( Position.createBefore( itemOrRange ), 1 ); + const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1; + + addRemoveDelta( Position.createBefore( itemOrRange ), howMany ); } return this; } + /** + * 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. + * + * @chainable + * @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 ); + + return this; + } + /** * Renames given element. * @@ -759,3 +774,11 @@ function addMarkerOperation( batch, name, oldRange, newRange ) { delta.addOperation( operation ); doc.applyOperation( operation ); } + +function isTheSameDocument( rootA, rootB ) { + if ( rootA === rootB ) { + return true; + } + + return rootA instanceof RootElement && rootB instanceof RootElement; +} diff --git a/tests/model/batch.js b/tests/model/batch.js index 094448e4d..372bfb2ac 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -236,57 +236,73 @@ describe( 'Batch', () => { expect( Array.from( parent.getChildren() ) ).to.deep.equal( [ child1, child2, child3 ] ); } ); - it( 'should move element from one parent to the other within different root', () => { - const parent1 = batch.createDocumentFragment(); - const parent2 = batch.createDocumentFragment(); - const b = batch.createText( 'b', { foo: 'bar' } ); + it( 'should create proper delta for inserting element', () => { + const parent = batch.createDocumentFragment(); + const element = batch.createElement( 'child' ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insert( element, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); + expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should create proper delta for inserting text', () => { + const parent = batch.createDocumentFragment(); + const text = batch.createText( 'child' ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.insert( text, parent ); + + sinon.assert.calledOnce( spy ); + expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); + expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); - batch - .insert( batch.createText( 'a' ), parent1 ) - .insert( b, parent1, 'end' ) - .insert( batch.createText( 'c' ), parent1, 'end' ); + it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => { + const rootA = doc.createRoot(); + const parent1 = batch.createElement( 'parent' ); + const parent2 = batch.createElement( 'parent' ); + const node = batch.createText( 'foo' ); - batch.insert( batch.createText( 'ac' ), parent2 ); + batch.insert( node, parent1 ); + batch.insert( parent1, rootA ); + batch.insert( parent2, rootA ); const spy = sinon.spy( doc, 'applyOperation' ); - batch.insert( b, parent2, 1 ); + batch.insert( node, parent2 ); // Verify result. - expect( Array.from( parent1, item => item.data ) ).to.deep.equal( [ 'ac' ] ); - expect( Array.from( parent2, item => item.data ) ).to.deep.equal( [ 'a', 'b', 'c' ] ); + expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] ); // Verify deltas and operations. - sinon.assert.calledTwice( spy ); - expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + sinon.assert.calledOnce( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' ); expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); - expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); - expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - it( 'should move element from one parent to the other within the same root', () => { - const root = batch.createDocumentFragment(); - const parent1 = batch.createElement( 'parent' ); - const parent2 = batch.createElement( 'parent' ); - const b = batch.createText( 'b', { foo: 'bar' } ); - - batch - .insert( parent1, root ) - .insert( batch.createText( 'a' ), parent1 ) - .insert( b, parent1, 'end' ) - .insert( batch.createText( 'c' ), parent1, 'end' ); + it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => { + const rootA = doc.createRoot( '$root', 'A' ); + const rootB = doc.createRoot( '$root', 'B' ); + const node = batch.createText( 'foo' ); - batch - .insert( parent2, root ) - .insert( batch.createText( 'ac' ), parent2 ); + batch.insert( node, rootA ); const spy = sinon.spy( doc, 'applyOperation' ); - batch.insert( b, parent2, 1 ); + batch.insert( node, rootB ); // Verify result. - expect( Array.from( parent1.getChildren(), item => item.data ) ).to.deep.equal( [ 'ac' ] ); - expect( Array.from( parent2.getChildren(), item => item.data ) ).to.deep.equal( [ 'a', 'b', 'c' ] ); + expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] ); // Verify deltas and operations. sinon.assert.calledOnce( spy ); @@ -294,51 +310,74 @@ describe( 'Batch', () => { expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - it( 'should create proper delta for element', () => { - const parent = batch.createDocumentFragment(); - const element = batch.createElement( 'child' ); + it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => { + const docFragA = batch.createDocumentFragment(); + const parent1 = batch.createElement( 'parent' ); + const parent2 = batch.createElement( 'parent' ); + const node = batch.createText( 'foo' ); + + batch.insert( node, parent1 ); + batch.insert( parent1, docFragA ); + batch.insert( parent2, docFragA ); const spy = sinon.spy( doc, 'applyOperation' ); - batch.insert( element, parent ); + batch.insert( node, parent2 ); + + // Verify result. + expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] ); + // Verify deltas and operations. sinon.assert.calledOnce( spy ); - expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); - expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); - expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta ); - expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - it( 'should create proper delta for text with no parent', () => { - const parent = batch.createDocumentFragment(); - const text = batch.createText( 'child' ); + it( 'should move element from one parent to the other within different document (document -> docFrag)', () => { + const root = doc.createRoot(); + const docFrag = batch.createDocumentFragment(); + const node = batch.createText( 'foo' ); + + batch.insert( node, root ); const spy = sinon.spy( doc, 'applyOperation' ); - batch.insert( text, parent ); + batch.insert( node, docFrag ); - sinon.assert.calledOnce( spy ); - expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); - expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); - expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + // Verify result. + expect( Array.from( root.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] ); + + // Verify deltas and operations. + sinon.assert.calledTwice( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - it( 'should create proper delta for text with parent', () => { - const parent1 = batch.createDocumentFragment(); - const parent2 = batch.createDocumentFragment(); - const text = batch.createText( 'child' ); + it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => { + const docFragA = batch.createDocumentFragment(); + const docFragB = batch.createDocumentFragment(); + const node = batch.createText( 'foo' ); - batch.insert( text, parent1 ); + batch.insert( node, docFragA ); const spy = sinon.spy( doc, 'applyOperation' ); - batch.insert( text, parent2 ); + batch.insert( node, docFragB ); + // Verify result. + expect( Array.from( docFragA ) ).to.deep.equal( [] ); + expect( Array.from( docFragB ) ).to.deep.equal( [ node ] ); + + // Verify deltas and operations. sinon.assert.calledTwice( spy ); - expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); - expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); - expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta ); - expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); it.skip( 'should transfer markers from given DocumentFragment', () => { @@ -654,53 +693,68 @@ describe( 'Batch', () => { expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - it( 'should move element from one parent to the other within different root', () => { - const parent1 = batch.createDocumentFragment(); - const parent2 = batch.createDocumentFragment(); - const b = batch.createText( 'b', { foo: 'bar' } ); + it( 'should move element from one parent to the other within the same document (documentA -> documentA)', () => { + const rootA = doc.createRoot(); + const parent1 = batch.createElement( 'parent' ); + const parent2 = batch.createElement( 'parent' ); + const node = batch.createText( 'foo' ); - batch - .append( batch.createText( 'a' ), parent1 ) - .append( b, parent1 ) - .append( batch.createText( 'c' ), parent1 ); + batch.insert( node, parent1 ); + batch.insert( parent1, rootA ); + batch.insert( parent2, rootA ); const spy = sinon.spy( doc, 'applyOperation' ); - batch.append( b, parent2, 1 ); + batch.append( node, parent2 ); // Verify result. - expect( Array.from( parent1, item => item.data ) ).to.deep.equal( [ 'ac' ] ); - expect( Array.from( parent2, item => item.data ) ).to.deep.equal( [ 'b' ] ); + expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] ); // Verify deltas and operations. - sinon.assert.calledTwice( spy ); - expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + sinon.assert.calledOnce( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' ); expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); - expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); - expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - it( 'should move element from one parent to the other within the same root', () => { - const root = batch.createDocumentFragment(); + it( 'should move element from one parent to the other within the same document (documentA -> documentB)', () => { + const rootA = doc.createRoot( '$root', 'A' ); + const rootB = doc.createRoot( '$root', 'B' ); + const node = batch.createText( 'foo' ); + + batch.insert( node, rootA ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.append( node, rootB ); + + // Verify result. + expect( Array.from( rootA.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( rootB.getChildren() ) ).to.deep.equal( [ node ] ); + + // Verify deltas and operations. + sinon.assert.calledOnce( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'move' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should move element from one parent to the other within the same document (docFragA -> docFragA)', () => { + const docFragA = batch.createDocumentFragment(); const parent1 = batch.createElement( 'parent' ); const parent2 = batch.createElement( 'parent' ); - const b = batch.createText( 'b', { foo: 'bar' } ); - - batch - .append( parent1, root ) - .append( batch.createText( 'a' ), parent1 ) - .append( b, parent1 ) - .append( batch.createText( 'c' ), parent1 ); + const node = batch.createText( 'foo' ); - batch.append( parent2, root ); + batch.insert( node, parent1 ); + batch.insert( parent1, docFragA ); + batch.insert( parent2, docFragA ); const spy = sinon.spy( doc, 'applyOperation' ); - batch.append( b, parent2 ); + batch.append( node, parent2 ); // Verify result. - expect( Array.from( parent1.getChildren(), item => item.data ) ).to.deep.equal( [ 'ac' ] ); - expect( Array.from( parent2.getChildren(), item => item.data ) ).to.deep.equal( [ 'b' ] ); + expect( Array.from( parent1.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( parent2.getChildren() ) ).to.deep.equal( [ node ] ); // Verify deltas and operations. sinon.assert.calledOnce( spy ); @@ -708,6 +762,52 @@ describe( 'Batch', () => { expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); + it( 'should move element from one parent to the other within different document (document -> docFrag)', () => { + const root = doc.createRoot(); + const docFrag = batch.createDocumentFragment(); + const node = batch.createText( 'foo' ); + + batch.insert( node, root ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.append( node, docFrag ); + + // Verify result. + expect( Array.from( root.getChildren() ) ).to.deep.equal( [] ); + expect( Array.from( docFrag.getChildren() ) ).to.deep.equal( [ node ] ); + + // Verify deltas and operations. + sinon.assert.calledTwice( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + + it( 'should move element from one parent to the other within different document (docFragA -> docFragB)', () => { + const docFragA = batch.createDocumentFragment(); + const docFragB = batch.createDocumentFragment(); + const node = batch.createText( 'foo' ); + + batch.insert( node, docFragA ); + + const spy = sinon.spy( doc, 'applyOperation' ); + + batch.append( node, docFragB ); + + // Verify result. + expect( Array.from( docFragA ) ).to.deep.equal( [] ); + expect( Array.from( docFragB ) ).to.deep.equal( [ node ] ); + + // Verify deltas and operations. + sinon.assert.calledTwice( spy ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); + expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); + } ); + it( 'should be chainable', () => { expect( batch.append( batch.createElement( 'a' ), batch.createElement( 'b' ) ) ).to.equal( batch ); } ); @@ -1209,6 +1309,91 @@ describe( 'Batch', () => { } ); } ); + describe( 'setAttributes()', () => { + let doc, batch, frag, item; + + beforeEach( () => { + doc = new Document(); + batch = doc.batch(); + + frag = batch.createDocumentFragment(); + item = batch.createText( 'xxx', { b: 2, c: 3 } ); + + batch.appendText( 'xxx', { a: 1 }, frag ); + batch.append( item, frag ); + } ); + + it( 'should set attributes one by one on range', () => { + const range = Range.createIn( frag ); + + // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate + // such a big amount of the same tests, so let's use a spy here. + const spy = sinon.spy( batch, 'setAttribute' ); + + batch.setAttributes( range, { a: 3, c: null } ); + + // Verify result. + expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] ); + expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] ); + + // Verify operations + sinon.assert.calledTwice( spy ); + sinon.assert.calledWith( spy.firstCall, range, 'a', 3 ); + sinon.assert.calledWith( spy.secondCall, range, 'c', null ); + } ); + + it( 'should set attributes one by one on range for map as attributes list', () => { + const range = Range.createIn( frag ); + + // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate + // such a big amount of the same tests, so let's use a spy here. + const spy = sinon.spy( batch, 'setAttribute' ); + + batch.setAttributes( range, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) ); + + // Verify result. + expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] ); + expect( Array.from( frag.getChild( 1 ).getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] ); + + // Verify operations + sinon.assert.calledTwice( spy ); + sinon.assert.calledWith( spy.firstCall, range, 'a', 3 ); + sinon.assert.calledWith( spy.secondCall, range, 'c', null ); + } ); + + it( 'should set attributes one by one on item', () => { + // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate + // such a big amount of the same tests, so let's use a spy here. + const spy = sinon.spy( batch, 'setAttribute' ); + + batch.setAttributes( item, { a: 3, c: null } ); + + // Verify result. + expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] ); + + // Verify operations + sinon.assert.calledTwice( spy ); + sinon.assert.calledWith( spy.firstCall, item, 'a', 3 ); + sinon.assert.calledWith( spy.secondCall, item, 'c', null ); + } ); + + it( 'should set attributes one by one on item for maps as attributes list', () => { + // `setAttribute` is a not trivial operation and is deeply tested above, there is no point to duplicate + // such a big amount of the same tests, so let's use a spy here. + const spy = sinon.spy( batch, 'setAttribute' ); + + batch.setAttributes( item, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) ); + + // Verify result. + expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] ); + + // Verify operations + sinon.assert.calledTwice( spy ); + sinon.assert.calledWith( spy.firstCall, item, 'a', 3 ); + sinon.assert.calledWith( spy.secondCall, item, 'c', null ); + } ); + } ); + describe( 'merge()', () => { let doc, root, p1, p2, batch; @@ -1299,6 +1484,14 @@ describe( 'Batch', () => { } ).to.throw( CKEditorError, /^batch-move-range-not-flat/ ); } ); + it( 'should throw if range is going to be moved to the other document', () => { + const docFrag = batch.createDocumentFragment(); + + expect( () => { + doc.batch().move( range, docFrag ); + } ).to.throw( CKEditorError, /^batch-move-different-document/ ); + } ); + it( 'should be chainable', () => { chain = batch.move( range, new Position( root, [ 1, 3 ] ) ); From f57ed3d62df33593c521f6ca5a5717843fab5daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 12:16:25 +0100 Subject: [PATCH 06/44] Added root getter to the AttributeOperation. --- src/model/operation/attributeoperation.js | 7 +++++ tests/model/operation/attributeoperation.js | 29 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/model/operation/attributeoperation.js b/src/model/operation/attributeoperation.js index 88ab99ad3..6d34fc95d 100644 --- a/src/model/operation/attributeoperation.js +++ b/src/model/operation/attributeoperation.js @@ -88,6 +88,13 @@ export default class AttributeOperation extends Operation { } } + /** + * @inheritDoc + */ + get root() { + return this.range.root; + } + /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/tests/model/operation/attributeoperation.js b/tests/model/operation/attributeoperation.js index e02569c2c..4a62bf329 100644 --- a/tests/model/operation/attributeoperation.js +++ b/tests/model/operation/attributeoperation.js @@ -59,6 +59,35 @@ describe( 'AttributeOperation', () => { } ); } ); + describe( 'root', () => { + it( 'should return root of range when range is in document', () => { + const op = new AttributeOperation( + new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ), + 'key', + 'oldValue', + 'newValue', + doc.version + ); + + expect( op.root ).to.equal( root ); + } ); + + it( 'should return root of range when range is in document fragment', () => { + const docFrag = doc.batch().createDocumentFragment(); + doc.batch().appendText( 'abc', null, docFrag ); + + const op = new AttributeOperation( + Range.createIn( docFrag ), + 'key', + 'oldValue', + 'newValue', + doc.version + ); + + expect( op.root ).to.equal( docFrag ); + } ); + } ); + it( 'should insert attribute to the set of nodes', () => { root.insertChildren( 0, new Text( 'bar' ) ); From 122b3ddcb7f382ec4e75a1a06c092d8cdf594831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 12:32:48 +0100 Subject: [PATCH 07/44] Added root getter to the InsertOperation. --- src/model/operation/insertoperation.js | 7 +++++++ tests/model/operation/insertoperation.js | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/model/operation/insertoperation.js b/src/model/operation/insertoperation.js index 26ff844e2..3d469ea69 100644 --- a/src/model/operation/insertoperation.js +++ b/src/model/operation/insertoperation.js @@ -55,6 +55,13 @@ export default class InsertOperation extends Operation { return 'insert'; } + /** + * @inheritDoc + */ + get root() { + return this.position.root; + } + /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/tests/model/operation/insertoperation.js b/tests/model/operation/insertoperation.js index d862cd04d..0aa0f202b 100644 --- a/tests/model/operation/insertoperation.js +++ b/tests/model/operation/insertoperation.js @@ -206,6 +206,30 @@ describe( 'InsertOperation', () => { expect( op2.nodes.getNode( 0 ) ).not.to.equal( text ); } ); + describe( 'root', () => { + it( 'should return operation root for document', () => { + const op = new InsertOperation( + new Position( root, [ 0 ] ), + new Text( 'x' ), + doc.version + ); + + expect( op.root ).to.equal( root ); + } ); + + it( 'should return operation root for document fragment', () => { + const docFrag = doc.batch().createDocumentFragment(); + + const op = new InsertOperation( + new Position( docFrag, [ 0 ] ), + new Text( 'x' ), + doc.version + ); + + expect( op.root ).to.equal( docFrag ); + } ); + } ); + describe( 'toJSON', () => { it( 'should create proper json object', () => { const position = new Position( root, [ 0 ] ); From fabcad2b9282f9b96c12fe5d4c1641cd41d8452c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 12:34:54 +0100 Subject: [PATCH 08/44] Added root getter to the MarkerOperation. --- src/model/operation/markeroperation.js | 7 +++++++ tests/model/operation/markeroperation.js | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/model/operation/markeroperation.js b/src/model/operation/markeroperation.js index 96b28d699..586cf7ea4 100644 --- a/src/model/operation/markeroperation.js +++ b/src/model/operation/markeroperation.js @@ -64,6 +64,13 @@ export default class MarkerOperation extends Operation { return 'marker'; } + /** + * @inheritDoc + */ + get root() { + return this.newRange ? this.newRange.root : this.oldRange.root; + } + /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/tests/model/operation/markeroperation.js b/tests/model/operation/markeroperation.js index 50abd0b06..25732caed 100644 --- a/tests/model/operation/markeroperation.js +++ b/tests/model/operation/markeroperation.js @@ -161,6 +161,20 @@ describe( 'MarkerOperation', () => { expect( clone ).to.deep.equal( op ); } ); + describe( 'type', () => { + it( 'should return root of new marker range when new marker is added', () => { + const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version ); + + expect( op.root ).to.equal( root ); + } ); + + it( 'should return root of old marker range when marker is removed', () => { + const op = new MarkerOperation( 'name', range, null, doc.markers, doc.version ); + + expect( op.root ).to.equal( root ); + } ); + } ); + describe( 'toJSON', () => { it( 'should create proper serialized object', () => { const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version ); From 692e79fae0c10d6ac3133435fb80a7deabd238d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 12:36:02 +0100 Subject: [PATCH 09/44] Added docs for root property to the Operation class. --- src/model/operation/operation.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/model/operation/operation.js b/src/model/operation/operation.js index dad5f3a46..eeb3e0f73 100644 --- a/src/model/operation/operation.js +++ b/src/model/operation/operation.js @@ -45,6 +45,15 @@ export default class Operation { * @member {module:engine/model/delta/delta~Delta} #delta */ + /** + * Root within operation is applied. It might be {@link module:engine/model/rootelement~RootElement RootElement} + * when operation is applied on {@link module:engine/model/document~Document Document} or any + * {module:engine/model/item~Item} when operation is applied on detached node. + * + * @readonly + * @member {module:engine/model/item~Item} #root + */ + /** * Creates and returns an operation that has the same parameters as this operation. * From 317a877fa1d67f5ca29acb6986df15979a78b993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 12:51:18 +0100 Subject: [PATCH 10/44] Added root getter to the MoveOperation. --- src/model/operation/moveoperation.js | 9 +++++++++ tests/model/operation/moveoperation.js | 28 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/model/operation/moveoperation.js b/src/model/operation/moveoperation.js index 2e03267cb..a45422c8c 100644 --- a/src/model/operation/moveoperation.js +++ b/src/model/operation/moveoperation.js @@ -73,6 +73,15 @@ export default class MoveOperation extends Operation { return 'move'; } + /** + * @inheritDoc + */ + get root() { + // Note that range cannot be moved within different documents e.g. from docFrag to document root so + // root of source and target positions will be always the same. + return this.targetPosition.root; + } + /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/tests/model/operation/moveoperation.js b/tests/model/operation/moveoperation.js index 708cd399c..7a970306a 100644 --- a/tests/model/operation/moveoperation.js +++ b/tests/model/operation/moveoperation.js @@ -267,6 +267,34 @@ describe( 'MoveOperation', () => { expect( clone.baseVersion ).to.equal( baseVersion ); } ); + describe( 'root', () => { + it( 'should return root for document', () => { + const op = new MoveOperation( + new Position( root, [ 0, 0 ] ), + 1, + new Position( root, [ 1, 0 ] ), + doc.version + ); + + expect( op.root ).to.equal( root ); + } ); + + it( 'should return root for document fragment', () => { + const docFrag = doc.batch().createDocumentFragment(); + + doc.batch().appendText( 'abc', null, docFrag ); + + const op = new MoveOperation( + new Position( docFrag, [ 0 ] ), + 1, + new Position( docFrag, [ 2 ] ), + doc.version + ); + + expect( op.root ).to.equal( docFrag ); + } ); + } ); + describe( 'getMovedRangeStart', () => { it( 'should return move operation target position transformed by removing move operation source range', () => { const sourcePosition = new Position( root, [ 0, 2 ] ); From ab481d16cb986c49693adab5a2a6bac2cb91c0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 12:59:25 +0100 Subject: [PATCH 11/44] Added root getter to the ReinsertOperation. --- src/model/operation/reinsertoperation.js | 7 +++++++ tests/model/operation/reinsertoperation.js | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/model/operation/reinsertoperation.js b/src/model/operation/reinsertoperation.js index d3bddfb2a..b461f1054 100644 --- a/src/model/operation/reinsertoperation.js +++ b/src/model/operation/reinsertoperation.js @@ -40,6 +40,13 @@ export default class ReinsertOperation extends MoveOperation { return 'reinsert'; } + /** + * @inheritDoc + */ + get root() { + return this.targetPosition.root; + } + /** * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. * diff --git a/tests/model/operation/reinsertoperation.js b/tests/model/operation/reinsertoperation.js index b08ca50b5..69444886a 100644 --- a/tests/model/operation/reinsertoperation.js +++ b/tests/model/operation/reinsertoperation.js @@ -99,6 +99,10 @@ describe( 'ReinsertOperation', () => { expect( graveyard.maxOffset ).to.equal( 2 ); } ); + it( 'should return root of operation', () => { + expect( operation.root ).to.equal( root ); + } ); + describe( 'toJSON', () => { it( 'should create proper json object', () => { const serialized = jsonParseStringify( operation ); From cc85957998e4b7b37e32bb71453181adc2f4d71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 14:36:11 +0100 Subject: [PATCH 12/44] Added isDocumentOperation property to the RemoveOperation. --- src/model/operation/removeoperation.js | 10 ++++++++++ tests/model/operation/removeoperation.js | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/model/operation/removeoperation.js b/src/model/operation/removeoperation.js index a08a32849..65fe79076 100644 --- a/src/model/operation/removeoperation.js +++ b/src/model/operation/removeoperation.js @@ -21,6 +21,16 @@ export default class RemoveOperation extends MoveOperation { return 'remove'; } + /** + * Remove operation cannot be applied on element that is not inside the document + * so this will always be a document operation. + * + * @member {Boolean} + */ + get isDocumentOperation() { + return true; + } + /** * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. * diff --git a/tests/model/operation/removeoperation.js b/tests/model/operation/removeoperation.js index 3553f2d46..3f949dcbc 100644 --- a/tests/model/operation/removeoperation.js +++ b/tests/model/operation/removeoperation.js @@ -140,6 +140,17 @@ describe( 'RemoveOperation', () => { expect( doc.graveyard.getChild( 2 ).name ).to.equal( 'y' ); } ); + it( 'should always be a document operation', () => { + const op = new RemoveOperation( + new Position( root, [ 2 ] ), + 2, + new Position( doc.graveyard, [ 0 ] ), + doc.version + ); + + expect( op.isDocumentOperation ).to.true; + } ); + describe( 'toJSON', () => { it( 'should create proper json object', () => { const op = new RemoveOperation( From 1674e0c45611b074da253efc4feb6a26aad9af2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 14:52:18 +0100 Subject: [PATCH 13/44] Throw when RemoveOperation is executed on detached item. --- src/model/operation/removeoperation.js | 19 +++++++++++++++++++ tests/model/operation/removeoperation.js | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/model/operation/removeoperation.js b/src/model/operation/removeoperation.js index 65fe79076..32ad7b6d2 100644 --- a/src/model/operation/removeoperation.js +++ b/src/model/operation/removeoperation.js @@ -9,6 +9,7 @@ import MoveOperation from './moveoperation'; import ReinsertOperation from './reinsertoperation'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * Operation to remove a range of nodes. @@ -42,6 +43,24 @@ export default class RemoveOperation extends MoveOperation { return new ReinsertOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 ); } + /** + * @inheritDoc + */ + _execute() { + if ( !this.sourcePosition.root.document ) { + /** + * Item that is going to be removed needs to be a {@link module:engine/model/document~Document document} child. + * To remove Item from detached document fragment use + * {@link module:engine/model/operation/detachoperation~DetachOperation DetachOperation}. + * + * @error remove-operation-on-detached-item + */ + throw new CKEditorError( 'remove-operation-on-detached-item: Cannot remove detached item.' ); + } + + return super._execute(); + } + /** * @inheritDoc */ diff --git a/tests/model/operation/removeoperation.js b/tests/model/operation/removeoperation.js index 3f949dcbc..33b755b7d 100644 --- a/tests/model/operation/removeoperation.js +++ b/tests/model/operation/removeoperation.js @@ -10,6 +10,7 @@ import MoveOperation from '../../../src/model/operation/moveoperation'; import Position from '../../../src/model/position'; import Text from '../../../src/model/text'; import Element from '../../../src/model/element'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils'; describe( 'RemoveOperation', () => { @@ -140,6 +141,25 @@ describe( 'RemoveOperation', () => { expect( doc.graveyard.getChild( 2 ).name ).to.equal( 'y' ); } ); + it( 'should throw when is executed on detached item', () => { + const batch = doc.batch(); + const docFrag = batch.createDocumentFragment(); + const item = batch.createElement( 'foo' ); + + batch.append( item, docFrag ); + + const op = new RemoveOperation( + new Position( docFrag, [ 0 ] ), + 1, + new Position( doc.graveyard, [ 0 ] ), + doc.version + ); + + expect( () => { + op._execute(); + } ).to.throw( CKEditorError, /^remove-operation-on-detached-item/ ); + } ); + it( 'should always be a document operation', () => { const op = new RemoveOperation( new Position( root, [ 2 ] ), From d7916c1b76445d07aff0c8217063489af2c95954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 16:20:03 +0100 Subject: [PATCH 14/44] Changed root property to isDocumentOperation. --- src/model/operation/attributeoperation.js | 4 +-- src/model/operation/insertoperation.js | 4 +-- src/model/operation/markeroperation.js | 6 ++-- src/model/operation/moveoperation.js | 4 +-- src/model/operation/nooperation.js | 7 ++++ src/model/operation/operation.js | 6 ++-- src/model/operation/reinsertoperation.js | 24 +++++++++++-- src/model/operation/renameoperation.js | 7 ++++ src/model/operation/rootattributeoperation.js | 10 ++++++ tests/model/operation/attributeoperation.js | 10 +++--- tests/model/operation/insertoperation.js | 10 +++--- tests/model/operation/markeroperation.js | 10 +++--- tests/model/operation/moveoperation.js | 10 +++--- tests/model/operation/nooperation.js | 4 +++ tests/model/operation/reinsertoperation.js | 35 +++++++++++++++++-- tests/model/operation/renameoperation.js | 19 ++++++++++ .../model/operation/rootattributeoperation.js | 28 +++++++++++++++ 17 files changed, 161 insertions(+), 37 deletions(-) diff --git a/src/model/operation/attributeoperation.js b/src/model/operation/attributeoperation.js index 6d34fc95d..52dc3bff0 100644 --- a/src/model/operation/attributeoperation.js +++ b/src/model/operation/attributeoperation.js @@ -91,8 +91,8 @@ export default class AttributeOperation extends Operation { /** * @inheritDoc */ - get root() { - return this.range.root; + get isDocumentOperation() { + return !!this.range.root.document; } /** diff --git a/src/model/operation/insertoperation.js b/src/model/operation/insertoperation.js index 3d469ea69..e8077e91c 100644 --- a/src/model/operation/insertoperation.js +++ b/src/model/operation/insertoperation.js @@ -58,8 +58,8 @@ export default class InsertOperation extends Operation { /** * @inheritDoc */ - get root() { - return this.position.root; + get isDocumentOperation() { + return !!this.position.root.document; } /** diff --git a/src/model/operation/markeroperation.js b/src/model/operation/markeroperation.js index 586cf7ea4..3a551a7bd 100644 --- a/src/model/operation/markeroperation.js +++ b/src/model/operation/markeroperation.js @@ -67,8 +67,10 @@ export default class MarkerOperation extends Operation { /** * @inheritDoc */ - get root() { - return this.newRange ? this.newRange.root : this.oldRange.root; + get isDocumentOperation() { + const root = this.newRange ? this.newRange.root : this.oldRange.root; + + return !!root.document; } /** diff --git a/src/model/operation/moveoperation.js b/src/model/operation/moveoperation.js index a45422c8c..cededd45a 100644 --- a/src/model/operation/moveoperation.js +++ b/src/model/operation/moveoperation.js @@ -76,10 +76,10 @@ export default class MoveOperation extends Operation { /** * @inheritDoc */ - get root() { + get isDocumentOperation() { // Note that range cannot be moved within different documents e.g. from docFrag to document root so // root of source and target positions will be always the same. - return this.targetPosition.root; + return !!this.targetPosition.root.document; } /** diff --git a/src/model/operation/nooperation.js b/src/model/operation/nooperation.js index 184f0b3e6..a79b37544 100644 --- a/src/model/operation/nooperation.js +++ b/src/model/operation/nooperation.js @@ -42,6 +42,13 @@ export default class NoOperation extends Operation { return new NoOperation( this.baseVersion + 1 ); } + /** + * @inheritDoc + */ + get isDocumentOperation() { + return true; + } + /** * @inheritDoc */ diff --git a/src/model/operation/operation.js b/src/model/operation/operation.js index eeb3e0f73..b5aa08a71 100644 --- a/src/model/operation/operation.js +++ b/src/model/operation/operation.js @@ -46,12 +46,10 @@ export default class Operation { */ /** - * Root within operation is applied. It might be {@link module:engine/model/rootelement~RootElement RootElement} - * when operation is applied on {@link module:engine/model/document~Document Document} or any - * {module:engine/model/item~Item} when operation is applied on detached node. + * Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}. * * @readonly - * @member {module:engine/model/item~Item} #root + * @member {Boolean} #isDocumentOperation */ /** diff --git a/src/model/operation/reinsertoperation.js b/src/model/operation/reinsertoperation.js index b461f1054..edc3304dc 100644 --- a/src/model/operation/reinsertoperation.js +++ b/src/model/operation/reinsertoperation.js @@ -9,6 +9,7 @@ import MoveOperation from './moveoperation'; import RemoveOperation from './removeoperation'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * Operation to reinsert previously removed nodes back to the non-graveyard root. This operation acts like @@ -41,10 +42,12 @@ export default class ReinsertOperation extends MoveOperation { } /** - * @inheritDoc + * Reinsert operation is always executed on attached items. + * + * @member {Boolean} */ - get root() { - return this.targetPosition.root; + get isDocumentOperation() { + return true; } /** @@ -58,6 +61,21 @@ export default class ReinsertOperation extends MoveOperation { return new RemoveOperation( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 ); } + /** + * @inheritDoc + */ + _execute() { + if ( !this.sourcePosition.root.document ) { + throw new CKEditorError( 'reinsert-operation-on-detached-item: Cannot reinsert detached item.' ); + } + + if ( !this.targetPosition.root.document ) { + throw new CKEditorError( 'reinsert-operation-to-detached-parent: Cannot reinsert item to detached parent.' ); + } + + return super._execute(); + } + /** * @inheritDoc */ diff --git a/src/model/operation/renameoperation.js b/src/model/operation/renameoperation.js index 502cd6828..bb39e6508 100644 --- a/src/model/operation/renameoperation.js +++ b/src/model/operation/renameoperation.js @@ -60,6 +60,13 @@ export default class RenameOperation extends Operation { return 'rename'; } + /** + * @inheritDoc + */ + get isDocumentOperation() { + return !!this.position.root.document; + } + /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/src/model/operation/rootattributeoperation.js b/src/model/operation/rootattributeoperation.js index 362f03be6..b29202491 100644 --- a/src/model/operation/rootattributeoperation.js +++ b/src/model/operation/rootattributeoperation.js @@ -69,6 +69,9 @@ export default class RootAttributeOperation extends Operation { this.newValue = newValue; } + /** + * @inheritDoc + */ get type() { if ( this.oldValue === null ) { return 'addRootAttribute'; @@ -79,6 +82,13 @@ export default class RootAttributeOperation extends Operation { } } + /** + * @inheritDoc + */ + get isDocumentOperation() { + return !!this.root.document; + } + /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/tests/model/operation/attributeoperation.js b/tests/model/operation/attributeoperation.js index 4a62bf329..ce46e0537 100644 --- a/tests/model/operation/attributeoperation.js +++ b/tests/model/operation/attributeoperation.js @@ -59,8 +59,8 @@ describe( 'AttributeOperation', () => { } ); } ); - describe( 'root', () => { - it( 'should return root of range when range is in document', () => { + describe( 'isDocumentOperation', () => { + it( 'should return true when attribute is applied on attached items', () => { const op = new AttributeOperation( new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) ), 'key', @@ -69,10 +69,10 @@ describe( 'AttributeOperation', () => { doc.version ); - expect( op.root ).to.equal( root ); + expect( op.isDocumentOperation ).to.true; } ); - it( 'should return root of range when range is in document fragment', () => { + it( 'should return false when attribute is applied on detached items', () => { const docFrag = doc.batch().createDocumentFragment(); doc.batch().appendText( 'abc', null, docFrag ); @@ -84,7 +84,7 @@ describe( 'AttributeOperation', () => { doc.version ); - expect( op.root ).to.equal( docFrag ); + expect( op.isDocumentOperation ).to.false; } ); } ); diff --git a/tests/model/operation/insertoperation.js b/tests/model/operation/insertoperation.js index 0aa0f202b..c0c04844f 100644 --- a/tests/model/operation/insertoperation.js +++ b/tests/model/operation/insertoperation.js @@ -206,18 +206,18 @@ describe( 'InsertOperation', () => { expect( op2.nodes.getNode( 0 ) ).not.to.equal( text ); } ); - describe( 'root', () => { - it( 'should return operation root for document', () => { + describe( 'isDocumentOperation', () => { + it( 'should return true when element is inserted to the document', () => { const op = new InsertOperation( new Position( root, [ 0 ] ), new Text( 'x' ), doc.version ); - expect( op.root ).to.equal( root ); + expect( op.isDocumentOperation ).to.true; } ); - it( 'should return operation root for document fragment', () => { + it( 'should return false when element is inserted to document fragment', () => { const docFrag = doc.batch().createDocumentFragment(); const op = new InsertOperation( @@ -226,7 +226,7 @@ describe( 'InsertOperation', () => { doc.version ); - expect( op.root ).to.equal( docFrag ); + expect( op.isDocumentOperation ).to.false; } ); } ); diff --git a/tests/model/operation/markeroperation.js b/tests/model/operation/markeroperation.js index 25732caed..4139ed8a3 100644 --- a/tests/model/operation/markeroperation.js +++ b/tests/model/operation/markeroperation.js @@ -161,17 +161,17 @@ describe( 'MarkerOperation', () => { expect( clone ).to.deep.equal( op ); } ); - describe( 'type', () => { - it( 'should return root of new marker range when new marker is added', () => { + describe( 'isDocumentOperation', () => { + it( 'should return true when new marker range is added to the document', () => { const op = new MarkerOperation( 'name', null, range, doc.markers, doc.version ); - expect( op.root ).to.equal( root ); + expect( op.isDocumentOperation ).to.true; } ); - it( 'should return root of old marker range when marker is removed', () => { + it( 'should return false when marker range is removed from the document', () => { const op = new MarkerOperation( 'name', range, null, doc.markers, doc.version ); - expect( op.root ).to.equal( root ); + expect( op.isDocumentOperation ).to.true; } ); } ); diff --git a/tests/model/operation/moveoperation.js b/tests/model/operation/moveoperation.js index 7a970306a..d45c49ba7 100644 --- a/tests/model/operation/moveoperation.js +++ b/tests/model/operation/moveoperation.js @@ -267,8 +267,8 @@ describe( 'MoveOperation', () => { expect( clone.baseVersion ).to.equal( baseVersion ); } ); - describe( 'root', () => { - it( 'should return root for document', () => { + describe( 'isDocumentOperation', () => { + it( 'should return root when operation is executed on attached items', () => { const op = new MoveOperation( new Position( root, [ 0, 0 ] ), 1, @@ -276,10 +276,10 @@ describe( 'MoveOperation', () => { doc.version ); - expect( op.root ).to.equal( root ); + expect( op.isDocumentOperation ).to.true; } ); - it( 'should return root for document fragment', () => { + it( 'should return false when operation is executed on detached items', () => { const docFrag = doc.batch().createDocumentFragment(); doc.batch().appendText( 'abc', null, docFrag ); @@ -291,7 +291,7 @@ describe( 'MoveOperation', () => { doc.version ); - expect( op.root ).to.equal( docFrag ); + expect( op.isDocumentOperation ).to.false; } ); } ); diff --git a/tests/model/operation/nooperation.js b/tests/model/operation/nooperation.js index 05e075640..6c7763319 100644 --- a/tests/model/operation/nooperation.js +++ b/tests/model/operation/nooperation.js @@ -37,6 +37,10 @@ describe( 'NoOperation', () => { expect( clone.baseVersion ).to.equal( 0 ); } ); + it( 'should be a document operation', () => { + expect( noop.isDocumentOperation ).to.true; + } ); + describe( 'toJSON', () => { it( 'should create proper json object', () => { const serialized = jsonParseStringify( noop ); diff --git a/tests/model/operation/reinsertoperation.js b/tests/model/operation/reinsertoperation.js index 69444886a..4040ceb42 100644 --- a/tests/model/operation/reinsertoperation.js +++ b/tests/model/operation/reinsertoperation.js @@ -10,6 +10,7 @@ import MoveOperation from '../../../src/model/operation/moveoperation'; import Position from '../../../src/model/position'; import Element from '../../../src/model/element'; import Text from '../../../src/model/text'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import { jsonParseStringify, wrapInDelta } from '../../../tests/model/_utils/utils'; describe( 'ReinsertOperation', () => { @@ -99,8 +100,38 @@ describe( 'ReinsertOperation', () => { expect( graveyard.maxOffset ).to.equal( 2 ); } ); - it( 'should return root of operation', () => { - expect( operation.root ).to.equal( root ); + it( 'should be a document operation', () => { + expect( operation.isDocumentOperation ).to.true; + } ); + + it( 'should throw when target position is not in the document', () => { + const docFrag = doc.batch().createDocumentFragment(); + + operation = new ReinsertOperation( + graveyardPosition, + 1, + Position.createAt( docFrag ), + doc.version + ); + + expect( () => { + operation._execute(); + } ).to.throw( CKEditorError, /^reinsert-operation-to-detached-parent/ ); + } ); + + it( 'should throw when source position is not in the document', () => { + const docFrag = doc.batch().createDocumentFragment(); + + operation = new ReinsertOperation( + Position.createAt( docFrag ), + 1, + rootPosition, + doc.version + ); + + expect( () => { + operation._execute(); + } ).to.throw( CKEditorError, /^reinsert-operation-on-detached-item/ ); } ); describe( 'toJSON', () => { diff --git a/tests/model/operation/renameoperation.js b/tests/model/operation/renameoperation.js index 8f91f8817..54e141564 100644 --- a/tests/model/operation/renameoperation.js +++ b/tests/model/operation/renameoperation.js @@ -92,6 +92,25 @@ describe( 'RenameOperation', () => { expect( clone.newName ).to.equal( newName ); } ); + describe( 'isDocumentOperation', () => { + it( 'should be true when target item is in the document', () => { + const op = new RenameOperation( position, oldName, newName, doc.version ); + + expect( op.isDocumentOperation ).to.true; + } ); + + it( 'should be false when target item is not in the document', () => { + const batch = doc.batch(); + const docFrag = batch.createDocumentFragment(); + + batch.appendElement( 'element', null, docFrag ); + + const op = new RenameOperation( Position.createAt( docFrag ), oldName, newName, doc.version ); + + expect( op.isDocumentOperation ).to.false; + } ); + } ); + describe( 'toJSON', () => { it( 'should create proper serialized object', () => { const op = new RenameOperation( Position.createAt( root, 'end' ), oldName, newName, doc.version ); diff --git a/tests/model/operation/rootattributeoperation.js b/tests/model/operation/rootattributeoperation.js index dfcbefa09..c7e43519d 100644 --- a/tests/model/operation/rootattributeoperation.js +++ b/tests/model/operation/rootattributeoperation.js @@ -54,6 +54,34 @@ describe( 'RootAttributeOperation', () => { } ); } ); + describe( 'isDocumentOperation', () => { + it( 'should be true when root is in the document', () => { + const operation = new RootAttributeOperation( + root, + 'isNew', + null, + true, + doc.version + ); + + expect( operation.isDocumentOperation ).to.true; + } ); + + it( 'should be false when root is not in the document', () => { + const element = doc.batch().createElement( 'element' ); + + const operation = new RootAttributeOperation( + element, + 'isNew', + null, + true, + doc.version + ); + + expect( operation.isDocumentOperation ).to.false; + } ); + } ); + it( 'should add attribute on the root element', () => { doc.applyOperation( wrapInDelta( new RootAttributeOperation( From 95c5dbf5f68f6e549f0b2c944cc1831e644128f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 16:21:20 +0100 Subject: [PATCH 15/44] Docs. --- src/model/document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/document.js b/src/model/document.js index 3f5152be2..1ca0a1bc4 100644 --- a/src/model/document.js +++ b/src/model/document.js @@ -419,7 +419,7 @@ export default class Document { /** * Fired when document changes by applying an operation. * - * There are 5 types of change: + * There are a few types of change: * * * 'insert' when nodes are inserted, * * 'remove' when nodes are removed, From efa281847e7dc11f3bc9f5beb8d6e848261a49f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 22 Nov 2017 16:59:09 +0100 Subject: [PATCH 16/44] Do not increase document version for non-document operations. --- src/model/document.js | 7 +++--- tests/model/document/document.js | 40 ++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/model/document.js b/src/model/document.js index 1ca0a1bc4..fe69cb56d 100644 --- a/src/model/document.js +++ b/src/model/document.js @@ -163,9 +163,10 @@ export default class Document { const changes = operation._execute(); - this.version++; - - this.history.addDelta( operation.delta ); + if ( operation.isDocumentOperation ) { + this.version++; + this.history.addDelta( operation.delta ); + } this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type ); } diff --git a/tests/model/document/document.js b/tests/model/document/document.js index 3c6281b7b..b552c1c26 100644 --- a/tests/model/document/document.js +++ b/tests/model/document/document.js @@ -110,7 +110,8 @@ describe( 'Document', () => { } ); describe( 'applyOperation()', () => { - it( 'should increase document version, execute operation and fire event with proper data', () => { + it( 'should increase document version, execute operation and fire event with proper data ' + + 'when operation is a document operation', () => { const changeCallback = sinon.spy(); const type = 't'; const data = { data: 'x' }; @@ -121,6 +122,7 @@ describe( 'Document', () => { const operation = { type, baseVersion: 0, + isDocumentOperation: true, _execute: sinon.stub().returns( data ) }; @@ -131,6 +133,40 @@ describe( 'Document', () => { doc.applyOperation( operation ); expect( doc.version ).to.equal( 1 ); + expect( doc.history._deltas.length ).to.equal( 1 ); + sinon.assert.calledOnce( operation._execute ); + + sinon.assert.calledOnce( changeCallback ); + expect( changeCallback.args[ 0 ][ 1 ] ).to.equal( type ); + expect( changeCallback.args[ 0 ][ 2 ] ).to.equal( data ); + expect( changeCallback.args[ 0 ][ 3 ] ).to.deep.equal( batch ); + expect( changeCallback.args[ 0 ][ 4 ] ).to.equal( delta.type ); + } ); + + it( 'should execute operation, fire event with proper data and not increase document version ' + + 'when operation is not a document operation', () => { + const changeCallback = sinon.spy(); + const type = 't'; + const data = { data: 'x' }; + const batch = new Batch(); + const delta = new Delta(); + delta.type = 'type'; + + const operation = { + type, + baseVersion: 0, + isDocumentOperation: false, + _execute: sinon.stub().returns( data ) + }; + + delta.addOperation( operation ); + batch.addDelta( delta ); + + doc.on( 'change', changeCallback ); + doc.applyOperation( operation ); + + expect( doc.version ).to.equal( 0 ); + expect( doc.history._deltas.length ).to.equal( 0 ); sinon.assert.calledOnce( operation._execute ); sinon.assert.calledOnce( changeCallback ); @@ -149,7 +185,7 @@ describe( 'Document', () => { () => { doc.applyOperation( operation ); } - ).to.throw( CKEditorError, /model-document-applyOperation-wrong-version/ ); + ).to.throw( CKEditorError, /^model-document-applyOperation-wrong-version/ ); } ); } ); From dfb4468d6fef3945a40cce475646018764023f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 12:54:02 +0100 Subject: [PATCH 17/44] Added DetachOperation. --- src/model/operation/detachoperation.js | 71 ++++++++++++++++++++++++ tests/model/operation/detachoperation.js | 55 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/model/operation/detachoperation.js create mode 100644 tests/model/operation/detachoperation.js diff --git a/src/model/operation/detachoperation.js b/src/model/operation/detachoperation.js new file mode 100644 index 000000000..1fe8adaf3 --- /dev/null +++ b/src/model/operation/detachoperation.js @@ -0,0 +1,71 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module engine/model/operation/detachoperation + */ + +import Operation from './operation'; +import { remove } from '../writer'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; + +/** + * Operation to permanently remove node from detached root. + * Note this operation is only a local operation and won't be send to the other clients. + * + * @extends module:engine/model/operation/operation~Operation + */ +export default class DetachOperation extends Operation { + /** + * Creates an insert operation. + * + * @param {module:engine/model/range~Range} range Range to remove. + * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied. + */ + constructor( range, baseVersion ) { + super( baseVersion ); + + /** + * Node to remove. + * + * @readonly + * @member {module:engine/model/range~Range} #range + */ + this.range = range; + } + + /** + * @inheritDoc + */ + get type() { + return 'detach'; + } + + /** + * @inheritDoc + */ + get isDocumentOperation() { + return false; + } + + /** + * @inheritDoc + */ + _execute() { + if ( this.range.root.document ) { + /** + * Cannot detach document node. + * Use {@link module:engine/model/operation/removeoperation~RemoveOperation remove operation} instead. + * + * @error detach-operation-on-document-node + */ + throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.' ); + } + + const nodes = remove( this.range ); + + return { nodes }; + } +} diff --git a/tests/model/operation/detachoperation.js b/tests/model/operation/detachoperation.js new file mode 100644 index 000000000..95ac17b2a --- /dev/null +++ b/tests/model/operation/detachoperation.js @@ -0,0 +1,55 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import Document from '../../../src/model/document'; +import DetachOperation from '../../../src/model/operation/detachoperation'; +import { wrapInDelta } from '../../../tests/model/_utils/utils'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import Range from '../../../src/model/range'; + +describe( 'DetachOperation', () => { + let doc, batch, docFrag, element; + + beforeEach( () => { + doc = new Document(); + batch = doc.batch(); + + docFrag = batch.createDocumentFragment(); + element = batch.createElement( 'element' ); + batch.append( element, docFrag ); + } ); + + it( 'should have type equal to detach', () => { + const op = new DetachOperation( element, doc.version ); + + expect( op.type ).to.equal( 'detach' ); + } ); + + it( 'should remove given element from parent', () => { + const op = new DetachOperation( Range.createOn( element ), doc.version ); + + doc.applyOperation( wrapInDelta( op ) ); + + expect( docFrag.childCount ).to.equal( 0 ); + } ); + + it( 'should throw when is executed on element from document', () => { + const root = doc.createRoot(); + const element = batch.createElement( 'element' ); + batch.append( element, root ); + + const op = new DetachOperation( Range.createOn( element ), doc.version ); + + expect( () => { + op._execute(); + } ).to.throw( CKEditorError, /^detach-operation-on-document-node/ ); + } ); + + it( 'should be not a document operation', () => { + const op = new DetachOperation( element, doc.version ); + + expect( op.isDocumentOperation ).to.false; + } ); +} ); From 9f6f9b1883b49a28344adaa7d4ca010b0195b2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 13:08:35 +0100 Subject: [PATCH 18/44] Aligned DataController with the new Batch API. --- src/controller/datacontroller.js | 8 ++++---- src/controller/deletecontent.js | 4 ++-- src/controller/insertcontent.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index fe17ce684..1bb558dfa 100644 --- a/src/controller/datacontroller.js +++ b/src/controller/datacontroller.js @@ -21,7 +21,6 @@ import { convertText, convertToModelFragment } from '../conversion/view-to-model import ViewDocumentFragment from '../view/documentfragment'; import ModelRange from '../model/range'; -import ModelPosition from '../model/position'; import ModelElement from '../model/element'; import insertContent from './insertcontent'; @@ -196,9 +195,10 @@ export default class DataController { this.model.selection.clearAttributes(); // Initial batch should be ignored by features like undo, etc. - this.model.batch( 'transparent' ) - .remove( ModelRange.createIn( modelRoot ) ) - .insert( ModelPosition.createAt( modelRoot, 0 ), this.parse( data ) ); + const batch = this.model.batch( 'transparent' ); + + batch.remove( ModelRange.createIn( modelRoot ) ); + batch.insert( this.parse( data ), modelRoot ); } ); } diff --git a/src/controller/deletecontent.js b/src/controller/deletecontent.js index 0664589e9..b03dbc7f2 100644 --- a/src/controller/deletecontent.js +++ b/src/controller/deletecontent.js @@ -128,7 +128,7 @@ function mergeBranches( batch, startPos, endPos ) { // x[]{}y // becomes: // x[]y{} - batch.move( endParent, startPos ); + batch.insert( endParent, startPos ); } // Merge two siblings: @@ -181,7 +181,7 @@ function checkCanBeMerged( leftPos, rightPos ) { function insertParagraph( batch, position, selection ) { const paragraph = new Element( 'paragraph' ); - batch.insert( position, paragraph ); + batch.insert( paragraph, position ); selection.setCollapsedAt( paragraph ); } diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 52a2e9902..9547592c8 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -256,7 +256,7 @@ class Insertion { const livePos = LivePosition.createFromPosition( this.position ); - this.batch.insert( this.position, node ); + this.batch.insert( node, this.position ); this.position = Position.createFromPosition( livePos ); livePos.detach(); From 8fa502510a29d5886b44db66cbb951197c95a22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 13:11:40 +0100 Subject: [PATCH 19/44] Aligned dev-utils with new Batch API. --- src/dev-utils/model.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 4259c8018..9fa8650e9 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -113,9 +113,10 @@ export function setData( document, data, options = {} ) { document.enqueueChanges( () => { // Replace existing model in document by new one. - document.batch( options.batchType || 'transparent' ) - .remove( ModelRange.createIn( modelRoot ) ) - .insert( ModelPosition.createAt( modelRoot, 0 ), modelDocumentFragment ); + const batch = document.batch( options.batchType || 'transparent' ); + + batch.remove( ModelRange.createIn( modelRoot ) ); + batch.insert( modelDocumentFragment, modelRoot ); // Clean up previous document selection. document.selection.clearAttributes(); From 023fdf2c7998563eb9ef31311cb68c5c1102ed23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 15:08:32 +0100 Subject: [PATCH 20/44] Used DetachOperation for removing detached items. --- src/model/batch.js | 17 +++-- tests/model/batch.js | 160 +++++++++++++++++++++++++++++++++---------- 2 files changed, 135 insertions(+), 42 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index afcc99d2d..6f24a4887 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -20,6 +20,7 @@ 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'; @@ -131,7 +132,7 @@ export default class Batch { } } - createText( data, attributes = {} ) { + createText( data, attributes ) { return new Text( data, attributes ); } @@ -315,11 +316,19 @@ export default class Batch { const addRemoveDelta = ( position, howMany ) => { const delta = new RemoveDelta(); this.addDelta( delta ); + let operation; - const graveyard = this.document.graveyard; - const gyPosition = new Position( graveyard, [ 0 ] ); + 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 { + const range = Range.createFromPositionAndShift( position, howMany ); + + operation = new DetachOperation( range, this.document.version ); + } - const operation = new RemoveOperation( position, howMany, gyPosition, this.document.version ); delta.addOperation( operation ); this.document.applyOperation( operation ); }; diff --git a/tests/model/batch.js b/tests/model/batch.js index 372bfb2ac..a48a81489 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -1511,65 +1511,149 @@ describe( 'Batch', () => { } ); describe( 'remove()', () => { - let doc, root, div, p, batch, chain, range; + let doc, batch, div, p, range; beforeEach( () => { doc = new Document(); - root = doc.createRoot(); - - div = new Element( 'div', [], new Text( 'foobar' ) ); - p = new Element( 'p', [], new Text( 'abcxyz' ) ); + batch = doc.batch(); - div.insertChildren( 0, [ new Element( 'p', [], new Text( 'gggg' ) ) ] ); - div.insertChildren( 2, [ new Element( 'p', [], new Text( 'hhhh' ) ) ] ); + div = batch.createElement( 'div' ); + batch.appendText( 'foobar', null, div ); - root.insertChildren( 0, [ div, p ] ); + p = batch.createElement( 'p' ); + batch.appendText( 'abcxyz', null, p ); - batch = doc.batch(); + batch.insertElement( 'p', null, div ); + batch.appendElement( 'p', null, div ); - // Range starts in ROOT > DIV > P > gg|gg. - // Range ends in ROOT > DIV > ...|ar. - range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) ); + batch.insertText( 'gggg', null, new Position( div, [ 0, 0 ] ) ); + batch.insertText( 'hhhh', null, new Position( div, [ 7, 0 ] ) ); } ); - it( 'should remove specified node', () => { - batch.remove( div ); + describe( 'remove from document', () => { + let root; - expect( root.maxOffset ).to.equal( 1 ); - expect( root.childCount ).to.equal( 1 ); - expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); - } ); + beforeEach( () => { + root = doc.createRoot(); + batch.append( div, root ); + batch.append( p, root ); - it( 'should remove any range of nodes', () => { - batch.remove( range ); + // Reset batch. + batch = doc.batch(); - expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' ); - expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' ); - } ); + // Range starts in ROOT > DIV > P > gg|gg. + // Range ends in ROOT > DIV > ...|ar. + range = new Range( new Position( root, [ 0, 0, 2 ] ), new Position( root, [ 0, 5 ] ) ); + } ); - it( 'should create minimal number of remove deltas, each with only one operation', () => { - batch.remove( range ); + it( 'should remove specified node', () => { + batch.remove( div ); - expect( batch.deltas.length ).to.equal( 2 ); - expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 ); - expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 ); - } ); + expect( root.maxOffset ).to.equal( 1 ); + expect( root.childCount ).to.equal( 1 ); + expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); + } ); - it( 'should be chainable', () => { - chain = batch.remove( range ); + it( 'should remove specified text node', () => { + batch.remove( p.getChild( 0 ) ); - expect( chain ).to.equal( batch ); + expect( getNodesAndText( Range.createOn( p ) ) ).to.equal( 'PP' ); + } ); + + it( 'should remove any range of nodes', () => { + batch.remove( range ); + + expect( getNodesAndText( Range.createIn( root.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' ); + expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcxyz' ); + } ); + + it( 'should create minimal number of remove deltas, each with only one operation', () => { + batch.remove( range ); + + expect( batch.deltas.length ).to.equal( 2 ); + expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 ); + expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.remove( div ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + + it( 'should use RemoveOperation', () => { + batch.remove( div ); + + expect( batch.deltas[ 0 ].operations[ 0 ].type ).to.equal( 'remove' ); + } ); } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.remove( div ); + describe( 'remove from document fragment', () => { + let frag; - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; + beforeEach( () => { + frag = batch.createDocumentFragment(); + batch.append( div, frag ); + batch.append( p, frag ); + + // Reset batch. + batch = doc.batch(); + + // Range starts in FRAG > DIV > P > gg|gg. + // Range ends in FRAG > DIV > ...|ar. + range = new Range( new Position( frag, [ 0, 0, 2 ] ), new Position( frag, [ 0, 5 ] ) ); } ); - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + it( 'should remove specified node', () => { + batch.remove( div ); + + expect( frag.maxOffset ).to.equal( 1 ); + expect( frag.childCount ).to.equal( 1 ); + expect( getNodesAndText( Range.createIn( frag.getChild( 0 ) ) ) ).to.equal( 'abcxyz' ); + } ); + + it( 'should remove specified text node', () => { + batch.remove( p.getChild( 0 ) ); + + expect( getNodesAndText( Range.createOn( p ) ) ).to.equal( 'PP' ); + } ); + + it( 'should remove any range of nodes', () => { + batch.remove( range ); + + expect( getNodesAndText( Range.createIn( frag.getChild( 0 ) ) ) ).to.equal( 'PggParPhhhhP' ); + expect( getNodesAndText( Range.createIn( frag.getChild( 1 ) ) ) ).to.equal( 'abcxyz' ); + } ); + + it( 'should create minimal number of remove deltas, each with only one operation', () => { + batch.remove( range ); + + expect( batch.deltas.length ).to.equal( 2 ); + expect( batch.deltas[ 0 ].operations.length ).to.equal( 1 ); + expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 ); + } ); + + it( 'should add delta to batch and operation to delta before applying operation', () => { + sinon.spy( doc, 'applyOperation' ); + batch.remove( div ); + + const correctDeltaMatcher = sinon.match( operation => { + return operation.delta && operation.delta.batch && operation.delta.batch == batch; + } ); + + expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; + } ); + + it( 'should use DetachOperation', () => { + batch.remove( div ); + + expect( batch.deltas[ 0 ].operations[ 0 ].type ).to.equal( 'detach' ); + } ); } ); } ); From e19a6df9cd720e80c2fa366cda64ea041e39b25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 17:06:43 +0100 Subject: [PATCH 21/44] Made Batch API not chainable. --- src/model/batch.js | 38 ++----- tests/model/batch.js | 258 +++++-------------------------------------- 2 files changed, 37 insertions(+), 259 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index 6f24a4887..77592c2cf 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -155,7 +155,9 @@ export default class Batch { // We need to check if item is going to be inserted within the same document. if ( isTheSameDocument( item.root, position.root ) ) { // If it's we just need to move it. - return this.move( Range.createOn( item ), position ); + this.move( Range.createOn( item ), position ); + + return; } // If it isn't the same root. else { @@ -183,28 +185,26 @@ export default class Batch { this.setMarker( markerName, range ); } } - - return this; } insertText( text, attributes, itemOrPosition, offset ) { - return this.insert( this.createText( text, attributes ), itemOrPosition, offset ); + this.insert( this.createText( text, attributes ), itemOrPosition, offset ); } insertElement( name, attributes, itemOrPosition, offset ) { - return this.insert( this.createElement( name, attributes ), itemOrPosition, offset ); + this.insert( this.createElement( name, attributes ), itemOrPosition, offset ); } append( item, parent ) { - return this.insert( item, parent, 'end' ); + this.insert( item, parent, 'end' ); } appendText( text, attributes, parent ) { - return this.insert( this.createText( text, attributes ), parent, 'end' ); + this.insert( this.createText( text, attributes ), parent, 'end' ); } appendElement( text, attributes, parent ) { - return this.insert( this.createElement( text, attributes ), parent, 'end' ); + this.insert( this.createElement( text, attributes ), parent, 'end' ); } /** @@ -223,8 +223,6 @@ export default class Batch { } else { setAttributeToItem( this, key, value, itemOrRange ); } - - return this; } setAttributes( itemOrRange, attributes ) { @@ -249,8 +247,6 @@ export default class Batch { } else { setAttributeToItem( this, key, null, itemOrRange ); } - - return this; } clearAttributes( itemOrRange ) { @@ -302,8 +298,6 @@ export default class Batch { const operation = new MoveOperation( range.start, range.end.offset - range.start.offset, position, this.document.version ); delta.addOperation( operation ); this.document.applyOperation( operation ); - - return this; } /** @@ -345,8 +339,6 @@ export default class Batch { addRemoveDelta( Position.createBefore( itemOrRange ), howMany ); } - - return this; } /** @@ -403,8 +395,6 @@ export default class Batch { const remove = new RemoveOperation( position, 1, gyPosition, this.document.version ); delta.addOperation( remove ); this.document.applyOperation( remove ); - - return this; } /** @@ -430,8 +420,6 @@ export default class Batch { const renameOperation = new RenameOperation( Position.createBefore( element ), element.name, newName, this.document.version ); delta.addOperation( renameOperation ); this.document.applyOperation( renameOperation ); - - return this; } /** @@ -479,8 +467,6 @@ export default class Batch { delta.addOperation( move ); this.document.applyOperation( move ); - - return this; } /** @@ -537,8 +523,6 @@ export default class Batch { ); delta.addOperation( move ); this.document.applyOperation( move ); - - return this; } /** @@ -582,8 +566,6 @@ export default class Batch { const remove = new RemoveOperation( Position.createBefore( element ), 1, gyPosition, this.document.version ); delta.addOperation( remove ); this.document.applyOperation( remove ); - - return this; } /** @@ -626,8 +608,6 @@ export default class Batch { // Just change marker range. addMarkerOperation( this, name, currentRange, newRange ); } - - return this; } /** @@ -651,8 +631,6 @@ export default class Batch { const oldRange = this.document.markers.get( name ).getRange(); addMarkerOperation( this, name, oldRange, null ); - - return this; } } diff --git a/tests/model/batch.js b/tests/model/batch.js index a48a81489..4fdc6613b 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -374,7 +374,7 @@ describe( 'Batch', () => { // Verify deltas and operations. sinon.assert.calledTwice( spy ); - expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'detach' ); expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); @@ -414,13 +414,6 @@ describe( 'Batch', () => { expect( doc.applyOperation.secondCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); expect( doc.applyOperation.thirdCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); } ); - - it( 'should be chainable', () => { - const parent = batch.createDocumentFragment(); - const child = batch.createElement( 'child' ); - - expect( batch.insert( child, parent ) ).to.equal( batch ); - } ); } ); describe( 'insertText()', () => { @@ -534,12 +527,6 @@ describe( 'Batch', () => { expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - - it( 'should be chainable', () => { - const parent = batch.createDocumentFragment(); - - expect( batch.insertText( 'foo', null, parent ) ).to.equal( batch ); - } ); } ); describe( 'insertElement()', () => { @@ -653,12 +640,6 @@ describe( 'Batch', () => { expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - - it( 'should be chainable', () => { - const parent = batch.createDocumentFragment(); - - expect( batch.insertElement( 'foo', null, parent ) ).to.equal( batch ); - } ); } ); describe( 'append()', () => { @@ -802,15 +783,11 @@ describe( 'Batch', () => { // Verify deltas and operations. sinon.assert.calledTwice( spy ); - expect( spy.firstCall.args[ 0 ].type ).to.equal( 'remove' ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'detach' ); expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); expect( spy.secondCall.args[ 0 ].type ).to.equal( 'insert' ); expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - - it( 'should be chainable', () => { - expect( batch.append( batch.createElement( 'a' ), batch.createElement( 'b' ) ) ).to.equal( batch ); - } ); } ); describe( 'appendText()', () => { @@ -845,22 +822,16 @@ describe( 'Batch', () => { expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); } ); - it( 'should create proper delta', () => { + it( 'should create proper delta and operations', () => { const parent = batch.createDocumentFragment(); const spy = sinon.spy( doc, 'applyOperation' ); batch.appendText( 'foo', null, parent ); sinon.assert.calledOnce( spy ); - expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); - expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); - expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); - } ); - - it( 'should be chainable', () => { - const parent = batch.createDocumentFragment(); - - expect( batch.appendText( 'foo', null, parent ) ).to.equal( batch ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.firstCall.args[ 0 ].delta ).to.instanceof( WeakInsertDelta ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); } ); @@ -895,23 +866,16 @@ describe( 'Batch', () => { expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); } ); - it( 'should create proper delta', () => { + it( 'should create proper delta and operation', () => { const parent = batch.createDocumentFragment(); const spy = sinon.spy( doc, 'applyOperation' ); batch.appendElement( 'foo', null, parent ); sinon.assert.calledOnce( spy ); - expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); - expect( spy.lastCall.args[ 0 ].delta ).to.instanceof( InsertDelta ); - expect( spy.lastCall.args[ 0 ].delta ).to.not.instanceof( WeakInsertDelta ); - expect( spy.lastCall.args[ 0 ].delta.batch ).to.equal( batch ); - } ); - - it( 'should be chainable', () => { - const parent = batch.createDocumentFragment(); - - expect( batch.appendElement( 'foo', null, parent ) ).to.equal( batch ); + expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' ); + expect( spy.firstCall.args[ 0 ].delta ).to.instanceof( InsertDelta ).to.not.instanceof( WeakInsertDelta ); + expect( spy.firstCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); } ); @@ -972,11 +936,6 @@ describe( 'Batch', () => { expect( node.getAttribute( 'a' ) ).to.equal( 1 ); } ); - it( 'should be chainable', () => { - const chain = batch.setAttribute( node, 'b', 2 ); - expect( chain ).to.equal( batch ); - } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { batch.setAttribute( node, 'b', 2 ); @@ -1002,11 +961,6 @@ describe( 'Batch', () => { expect( spy.callCount ).to.equal( 0 ); } ); - it( 'should be chainable', () => { - const chain = batch.removeAttribute( node, 'a' ); - expect( chain ).to.equal( batch ); - } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { batch.removeAttribute( node, 'a' ); @@ -1019,16 +973,15 @@ describe( 'Batch', () => { beforeEach( () => { const element = batch.createElement( 'e', { a: 2 } ); - batch - .appendText( 'xxx', { a: 1 }, root ) - .appendText( 'xxx', null, root ) - .appendText( 'xxx', { a: 1 }, root ) - .appendText( 'xxx', { a: 2 }, root ) - .appendText( 'xxx', null, root ) - .appendText( 'xxx', { a: 1 }, root ) - .appendText( 'xxx', null, element ) - .append( element, root ) - .appendText( 'xxx', null, root ); + batch.appendText( 'xxx', { a: 1 }, root ); + batch.appendText( 'xxx', null, root ); + batch.appendText( 'xxx', { a: 1 }, root ); + batch.appendText( 'xxx', { a: 2 }, root ); + batch.appendText( 'xxx', null, root ); + batch.appendText( 'xxx', { a: 1 }, root ); + batch.appendText( 'xxx', null, element ); + batch.append( element, root ); + batch.appendText( 'xxx', null, root ); spy = sinon.spy( doc, 'applyOperation' ); } ); @@ -1153,11 +1106,6 @@ describe( 'Batch', () => { expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' ); } ); - it( 'should be chainable', () => { - const chain = batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); - expect( chain ).to.equal( batch ); - } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); @@ -1239,11 +1187,6 @@ describe( 'Batch', () => { expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); } ); - it( 'should be chainable', () => { - const chain = batch.removeAttribute( getRange( 0, 2 ), 'a' ); - expect( chain ).to.equal( batch ); - } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { batch.removeAttribute( getRange( 0, 2 ), 'a' ); sinon.assert.calledWith( spy, correctDeltaMatcher ); @@ -1395,7 +1338,7 @@ describe( 'Batch', () => { } ); describe( 'merge()', () => { - let doc, root, p1, p2, batch; + let doc, root, p1, p2; beforeEach( () => { doc = new Document(); @@ -1429,28 +1372,10 @@ describe( 'Batch', () => { doc.batch().merge( new Position( root, [ 0, 2 ] ) ); } ).to.throw( CKEditorError, /^batch-merge-no-element-before/ ); } ); - - it( 'should be chainable', () => { - batch = doc.batch(); - - const chain = batch.merge( new Position( root, [ 1 ] ) ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch = doc.batch().merge( new Position( root, [ 1 ] ) ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); describe( 'move()', () => { - let doc, root, range, div, p, batch, chain; + let doc, root, range, div, p, batch; beforeEach( () => { doc = new Document(); @@ -1491,23 +1416,6 @@ describe( 'Batch', () => { doc.batch().move( range, docFrag ); } ).to.throw( CKEditorError, /^batch-move-different-document/ ); } ); - - it( 'should be chainable', () => { - chain = batch.move( range, new Position( root, [ 1, 3 ] ) ); - - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.move( range, new Position( root, [ 2 ] ) ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); describe( 'remove()', () => { @@ -1575,17 +1483,6 @@ describe( 'Batch', () => { expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 ); } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.remove( div ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - it( 'should use RemoveOperation', () => { batch.remove( div ); @@ -1638,17 +1535,6 @@ describe( 'Batch', () => { expect( batch.deltas[ 1 ].operations.length ).to.equal( 1 ); } ); - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.remove( div ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); - it( 'should use DetachOperation', () => { batch.remove( div ); @@ -1658,7 +1544,7 @@ describe( 'Batch', () => { } ); describe( 'rename()', () => { - let doc, root, batch, chain; + let doc, root, batch; beforeEach( () => { doc = new Document(); @@ -1669,13 +1555,12 @@ describe( 'Batch', () => { batch = doc.batch(); - chain = batch.rename( p, 'h' ); + batch.rename( p, 'h' ); } ); it( 'should rename given element', () => { expect( root.maxOffset ).to.equal( 1 ); expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' ); - expect( root.getChild( 0 ) ).to.have.property( 'name', 'h' ); } ); it( 'should throw if not an Element instance is passed', () => { @@ -1683,21 +1568,6 @@ describe( 'Batch', () => { batch.rename( new Text( 'abc' ), 'h' ); } ).to.throw( CKEditorError, /^batch-rename-not-element-instance/ ); } ); - - it( 'should be chainable', () => { - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - batch.rename( root.getChild( 0 ), 'p' ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.alwaysCalledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); describe( 'split()', () => { @@ -1752,24 +1622,6 @@ describe( 'Batch', () => { doc.batch().split( new Position( root, [ 0 ] ) ); } ).to.throw( CKEditorError, /^batch-split-root/ ); } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - - const chain = batch.split( new Position( root, [ 0, 3 ] ) ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().split( new Position( root, [ 0, 3 ] ) ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); describe( 'wrap()', () => { @@ -1814,7 +1666,7 @@ describe( 'Batch', () => { } ).to.throw( CKEditorError, /^batch-wrap-range-not-flat/ ); } ); - it( 'should throw if element to wrap with has children', () => { + it( 'should throw if element to wrap with has children #1', () => { const p = new Element( 'p', [], new Text( 'a' ) ); expect( () => { @@ -1822,7 +1674,7 @@ describe( 'Batch', () => { } ).to.throw( CKEditorError, /^batch-wrap-element-not-empty/ ); } ); - it( 'should throw if element to wrap with has children', () => { + it( 'should throw if element to wrap with has children #2', () => { const p = new Element( 'p' ); root.insertChildren( 0, p ); @@ -1830,24 +1682,6 @@ describe( 'Batch', () => { doc.batch().wrap( range, p ); } ).to.throw( CKEditorError, /^batch-wrap-element-attached/ ); } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - - const chain = batch.wrap( range, 'p' ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().wrap( range, 'p' ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); describe( 'unwrap()', () => { @@ -1875,24 +1709,6 @@ describe( 'Batch', () => { doc.batch().unwrap( element ); } ).to.throw( CKEditorError, /^batch-unwrap-element-no-parent/ ); } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - - const chain = batch.unwrap( p ); - expect( chain ).to.equal( batch ); - } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().unwrap( p ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); describe( 'setMarker()', () => { @@ -1925,7 +1741,8 @@ describe( 'Batch', () => { const marker = doc.markers.get( 'name' ); const range2 = Range.createFromParentsAndOffsets( root, 0, root, 0 ); - const batch = doc.batch().setMarker( marker, range2 ); + const batch = doc.batch(); + batch.setMarker( marker, range2 ); const op = batch.deltas[ 0 ].operations[ 0 ]; expect( doc.markers.get( 'name' ).getRange().isEqual( range2 ) ).to.be.true; @@ -1945,7 +1762,8 @@ describe( 'Batch', () => { } } ); - const batch = doc.batch().setMarker( marker ); + const batch = doc.batch(); + batch.setMarker( marker ); const op = batch.deltas[ 0 ].operations[ 0 ]; expect( doc.fire.calledWith( 'change', 'marker' ) ).to.be.true; @@ -1958,13 +1776,6 @@ describe( 'Batch', () => { doc.batch().setMarker( 'name' ); } ).to.throw( CKEditorError, /^batch-setMarker-no-range/ ); } ); - - it( 'should be chainable', () => { - const batch = doc.batch(); - const chain = batch.setMarker( 'name', range ); - - expect( chain ).to.equal( batch ); - } ); } ); describe( 'removeMarker()', () => { @@ -1998,16 +1809,5 @@ describe( 'Batch', () => { expect( doc.markers.get( 'name' ) ).to.be.null; } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - sinon.spy( doc, 'applyOperation' ); - const batch = doc.batch().setMarker( 'name', range ); - - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); } ); From 05666ea35fa3e099621096be2676fc16dde80186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 19:33:41 +0100 Subject: [PATCH 22/44] Made attributes as a optional append* and insert* parameter. --- src/model/batch.js | 29 +++++++++----- tests/model/batch.js | 91 ++++++++++++++++++++++++++++++++------------ 2 files changed, 87 insertions(+), 33 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index 77592c2cf..6b8065240 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -188,11 +188,19 @@ export default class Batch { } insertText( text, attributes, itemOrPosition, offset ) { - this.insert( this.createText( 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 ); + } } insertElement( name, attributes, itemOrPosition, offset ) { - this.insert( this.createElement( 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 ); + } } append( item, parent ) { @@ -200,11 +208,19 @@ export default class Batch { } appendText( text, attributes, parent ) { - this.insert( this.createText( text, attributes ), parent, 'end' ); + if ( attributes instanceof DocumentFragment || attributes instanceof Element ) { + this.insert( this.createText( text ), attributes, 'end' ); + } else { + this.insert( this.createText( text, attributes ), parent, 'end' ); + } } appendElement( text, attributes, parent ) { - this.insert( this.createElement( text, attributes ), parent, 'end' ); + if ( attributes instanceof DocumentFragment || attributes instanceof Element ) { + this.insert( this.createElement( text ), attributes, 'end' ); + } else { + this.insert( this.createElement( text, attributes ), parent, 'end' ); + } } /** @@ -265,11 +281,6 @@ export default class Batch { } } - /** - * Moves given {@link module:engine/model/item~Item model item} or given range to target position. - * - * @chainable - */ move( range, itemOrPosition, offset ) { if ( !range.isFlat ) { /** diff --git a/tests/model/batch.js b/tests/model/batch.js index 4fdc6613b..552e5ead2 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -446,12 +446,23 @@ describe( 'Batch', () => { expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); } ); + it( 'should create and insert text node omitting attributes param', () => { + const parent = batch.createDocumentFragment(); + + batch.insertText( 'foo', new Position( parent, [ 0 ] ) ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Text ); + expect( parent.getChild( 0 ).data ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); + } ); + it( 'should create and insert text node at the beginning of given element', () => { const parent = batch.createDocumentFragment(); batch.insert( batch.createElement( 'child' ), parent ); - batch.insertText( 'foo', null, parent ); + batch.insertText( 'foo', parent ); expect( parent.childCount ).to.equal( 2 ); expect( parent.getChild( 0 ) ).to.instanceof( Text ); @@ -463,7 +474,7 @@ describe( 'Batch', () => { batch.insert( batch.createElement( 'child' ), parent ); - batch.insertText( 'foo', null, parent, 'end' ); + batch.insertText( 'foo', parent, 'end' ); expect( parent.childCount ).to.equal( 2 ); expect( parent.getChild( 0 ) ).to.instanceof( Element ); @@ -476,7 +487,7 @@ describe( 'Batch', () => { batch.insert( batch.createElement( 'child' ), parent ); batch.insert( batch.createElement( 'child' ), parent ); - batch.insertText( 'foo', null, parent, 1 ); + batch.insertText( 'foo', parent, 1 ); expect( parent.childCount ).to.equal( 3 ); expect( parent.getChild( 0 ) ).to.instanceof( Element ); @@ -492,7 +503,7 @@ describe( 'Batch', () => { batch.insert( child1, parent ); batch.insert( child2, parent, 'end' ); - batch.insertText( 'foo', null, child2, 'before' ); + batch.insertText( 'foo', child2, 'before' ); expect( parent.childCount ).to.equal( 3 ); expect( parent.getChild( 0 ) ).to.instanceof( Element ); @@ -508,7 +519,7 @@ describe( 'Batch', () => { batch.insert( child1, parent ); batch.insert( child2, parent, 'end' ); - batch.insertText( 'foo', null, child1, 'after' ); + batch.insertText( 'foo', child1, 'after' ); expect( parent.childCount ).to.equal( 3 ); expect( parent.getChild( 0 ) ).to.instanceof( Element ); @@ -520,7 +531,7 @@ describe( 'Batch', () => { const parent = batch.createDocumentFragment(); const spy = sinon.spy( doc, 'applyOperation' ); - batch.insertText( 'foo', null, parent ); + batch.insertText( 'foo', parent ); sinon.assert.calledOnce( spy ); expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); @@ -559,12 +570,23 @@ describe( 'Batch', () => { expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); } ); + it( 'should create and insert element with no attributes omitting attributes param', () => { + const parent = batch.createDocumentFragment(); + + batch.insertElement( 'foo', new Position( parent, [ 0 ] ) ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Element ); + expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); + } ); + it( 'should create and insert element at the beginning of given element', () => { const parent = batch.createDocumentFragment(); batch.insert( batch.createElement( 'child' ), parent ); - batch.insertElement( 'foo', null, parent ); + batch.insertElement( 'foo', parent ); expect( parent.childCount ).to.equal( 2 ); expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); @@ -576,7 +598,7 @@ describe( 'Batch', () => { batch.insert( batch.createElement( 'child' ), parent ); - batch.insertElement( 'foo', null, parent, 'end' ); + batch.insertElement( 'foo', parent, 'end' ); expect( parent.childCount ).to.equal( 2 ); expect( parent.getChild( 0 ).name ).to.equal( 'child' ); @@ -589,7 +611,7 @@ describe( 'Batch', () => { batch.insert( batch.createElement( 'child1' ), parent ); batch.insert( batch.createElement( 'child2' ), parent, 'end' ); - batch.insertElement( 'foo', null, parent, 1 ); + batch.insertElement( 'foo', parent, 1 ); expect( parent.childCount ).to.equal( 3 ); expect( parent.getChild( 0 ).name ).to.equal( 'child1' ); @@ -605,7 +627,7 @@ describe( 'Batch', () => { batch.insert( child1, parent ); batch.insert( child2, parent, 'end' ); - batch.insertElement( 'foo', null, child2, 'before' ); + batch.insertElement( 'foo', child2, 'before' ); expect( parent.childCount ).to.equal( 3 ); expect( parent.getChild( 0 ).name ).to.equal( 'child1' ); @@ -621,7 +643,7 @@ describe( 'Batch', () => { batch.insert( child1, parent ); batch.insert( child2, parent, 'end' ); - batch.insertElement( 'foo', null, child1, 'after' ); + batch.insertElement( 'foo', child1, 'after' ); expect( parent.childCount ).to.equal( 3 ); expect( parent.getChild( 0 ).name ).to.equal( 'child1' ); @@ -633,7 +655,7 @@ describe( 'Batch', () => { const parent = batch.createDocumentFragment(); const spy = sinon.spy( doc, 'applyOperation' ); - batch.insertText( 'foo', null, parent ); + batch.insertText( 'foo', parent ); sinon.assert.calledOnce( spy ); expect( spy.lastCall.args[ 0 ].type ).to.equal( 'insert' ); @@ -822,11 +844,22 @@ describe( 'Batch', () => { expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); } ); + it( 'should create and insert text node with no attributes omitting attributes param', () => { + const parent = batch.createDocumentFragment(); + + batch.appendText( 'foo', parent ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ) ).to.instanceof( Text ); + expect( parent.getChild( 0 ).data ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); + } ); + it( 'should create proper delta and operations', () => { const parent = batch.createDocumentFragment(); const spy = sinon.spy( doc, 'applyOperation' ); - batch.appendText( 'foo', null, parent ); + batch.appendText( 'foo', parent ); sinon.assert.calledOnce( spy ); expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' ); @@ -866,11 +899,21 @@ describe( 'Batch', () => { expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); } ); + it( 'should create and insert element with no attributes omitting attributes param', () => { + const parent = batch.createDocumentFragment(); + + batch.appendElement( 'foo', parent ); + + expect( parent.childCount ).to.equal( 1 ); + expect( parent.getChild( 0 ).name ).to.equal( 'foo' ); + expect( Array.from( parent.getChild( 0 ).getAttributes() ) ).to.deep.equal( [] ); + } ); + it( 'should create proper delta and operation', () => { const parent = batch.createDocumentFragment(); const spy = sinon.spy( doc, 'applyOperation' ); - batch.appendElement( 'foo', null, parent ); + batch.appendElement( 'foo', parent ); sinon.assert.calledOnce( spy ); expect( spy.firstCall.args[ 0 ].type ).to.equal( 'insert' ); @@ -974,14 +1017,14 @@ describe( 'Batch', () => { const element = batch.createElement( 'e', { a: 2 } ); batch.appendText( 'xxx', { a: 1 }, root ); - batch.appendText( 'xxx', null, root ); + batch.appendText( 'xxx', root ); batch.appendText( 'xxx', { a: 1 }, root ); batch.appendText( 'xxx', { a: 2 }, root ); - batch.appendText( 'xxx', null, root ); + batch.appendText( 'xxx', root ); batch.appendText( 'xxx', { a: 1 }, root ); - batch.appendText( 'xxx', null, element ); + batch.appendText( 'xxx', element ); batch.append( element, root ); - batch.appendText( 'xxx', null, root ); + batch.appendText( 'xxx', root ); spy = sinon.spy( doc, 'applyOperation' ); } ); @@ -1426,16 +1469,16 @@ describe( 'Batch', () => { batch = doc.batch(); div = batch.createElement( 'div' ); - batch.appendText( 'foobar', null, div ); + batch.appendText( 'foobar', div ); p = batch.createElement( 'p' ); - batch.appendText( 'abcxyz', null, p ); + batch.appendText( 'abcxyz', p ); - batch.insertElement( 'p', null, div ); - batch.appendElement( 'p', null, div ); + batch.insertElement( 'p', div ); + batch.appendElement( 'p', div ); - batch.insertText( 'gggg', null, new Position( div, [ 0, 0 ] ) ); - batch.insertText( 'hhhh', null, new Position( div, [ 7, 0 ] ) ); + batch.insertText( 'gggg', new Position( div, [ 0, 0 ] ) ); + batch.insertText( 'hhhh', new Position( div, [ 7, 0 ] ) ); } ); describe( 'remove from document', () => { From 4dc8d92e0f840670cbcdd79bf355df7bacc47134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 19:34:32 +0100 Subject: [PATCH 23/44] Throw when range to move is invalid. --- src/model/batch.js | 9 +++++++++ tests/model/batch.js | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/model/batch.js b/src/model/batch.js index 6b8065240..b643896ba 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -282,6 +282,15 @@ export default class Batch { } 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. diff --git a/tests/model/batch.js b/tests/model/batch.js index 552e5ead2..aa83dc112 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -1444,6 +1444,12 @@ describe( 'Batch', () => { expect( getNodesAndText( Range.createIn( root.getChild( 1 ) ) ) ).to.equal( 'abcobarxyz' ); } ); + it( 'should throw if object to move is not a range', () => { + expect( () => { + doc.batch().move( root.getChild( 0 ), new Position( root, [ 1, 3 ] ) ); + } ).to.throw( CKEditorError, /^batch-move-invalid-range/ ); + } ); + it( 'should throw if given range is not flat', () => { const notFlatRange = new Range( new Position( root, [ 0, 2, 2 ] ), new Position( root, [ 0, 6 ] ) ); From 31c8ff54be8b4888adc3d70c87280519b61c66f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 19:35:37 +0100 Subject: [PATCH 24/44] Mark MarkerOperation as document operation when there is no range to remove. --- src/model/operation/markeroperation.js | 11 +++++++++-- tests/model/operation/markeroperation.js | 6 ++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/model/operation/markeroperation.js b/src/model/operation/markeroperation.js index 3a551a7bd..d7991d3e5 100644 --- a/src/model/operation/markeroperation.js +++ b/src/model/operation/markeroperation.js @@ -68,9 +68,16 @@ export default class MarkerOperation extends Operation { * @inheritDoc */ get isDocumentOperation() { - const root = this.newRange ? this.newRange.root : this.oldRange.root; + if ( this.newRange ) { + return !!this.newRange.root.document; + } - return !!root.document; + if ( this.oldRange ) { + return !!this.oldRange.root.document; + } + + // This is edge and might happen only on data from the server. + return true; } /** diff --git a/tests/model/operation/markeroperation.js b/tests/model/operation/markeroperation.js index 4139ed8a3..1434b858e 100644 --- a/tests/model/operation/markeroperation.js +++ b/tests/model/operation/markeroperation.js @@ -173,6 +173,12 @@ describe( 'MarkerOperation', () => { expect( op.isDocumentOperation ).to.true; } ); + + it( 'should return true when non-existing marker range is removed from the document', () => { + const op = new MarkerOperation( 'name', null, null, doc.markers, doc.version ); + + expect( op.isDocumentOperation ).to.true; + } ); } ); describe( 'toJSON', () => { From 7f7d2f44fa65739646aed3f1623dc2a1cc0cc177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 20:04:21 +0100 Subject: [PATCH 25/44] Aligned engine code with new Batch API. --- src/model/document.js | 4 +- tests/controller/editingcontroller.js | 6 +-- tests/conversion/modelconversiondispatcher.js | 38 +++++++++++-------- tests/model/documentselection.js | 10 ++--- tests/model/markercollection.js | 2 +- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/model/document.js b/src/model/document.js index fe69cb56d..018d1e1c7 100644 --- a/src/model/document.js +++ b/src/model/document.js @@ -37,7 +37,9 @@ const graveyardName = '$graveyard'; * All changes in the document are done by {@link module:engine/model/operation/operation~Operation operations}. To create operations in * a simple way, use the {@link module:engine/model/batch~Batch} API, for example: * - * doc.batch().insert( position, nodes ).split( otherPosition ); + * const batch = doc.batch(); + * batch.insert( node, position ); + * batch.split( otherPosition ); * * @see module:engine/model/document~Document#batch * @mixes module:utils/emittermixin~EmitterMixin diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js index a0ae8b850..43180b503 100644 --- a/tests/controller/editingcontroller.js +++ b/tests/controller/editingcontroller.js @@ -159,7 +159,7 @@ describe( 'EditingController', () => { )._children ); model.enqueueChanges( () => { - model.batch().insert( ModelPosition.createAt( model.getRoot(), 0 ), modelData ); + model.batch().insert( modelData, model.getRoot() ); model.selection.addRange( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 1, modelRoot.getChild( 0 ), 1 ) ); } ); @@ -375,7 +375,7 @@ describe( 'EditingController', () => { it( 'should forward add marker event if content is moved into a marker range', () => { model.enqueueChanges( () => { - model.batch().insert( ModelPosition.createAt( model.getRoot(), 'end' ), new ModelElement( 'paragraph' ) ); + model.batch().appendElement( 'paragraph', model.getRoot() ); } ); const markerRange = ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, 3 ); @@ -411,7 +411,7 @@ describe( 'EditingController', () => { model.enqueueChanges( () => { const modelData = parse( 'foo', model.schema ).getChild( 0 ); - model.batch().insert( ModelPosition.createAt( model.getRoot(), 0 ), modelData ); + model.batch().insert( modelData, model.getRoot() ); } ); expect( spy.called ).to.be.false; diff --git a/tests/conversion/modelconversiondispatcher.js b/tests/conversion/modelconversiondispatcher.js index 32c98c9af..dac0eb408 100644 --- a/tests/conversion/modelconversiondispatcher.js +++ b/tests/conversion/modelconversiondispatcher.js @@ -63,8 +63,7 @@ describe( 'ModelConversionDispatcher', () => { dispatcher.on( 'insert:image', cbInsertImage ); dispatcher.on( 'addAttribute:key:$text', cbAddAttribute ); - const insertedText = new ModelText( 'foo', { key: 'value' } ); - doc.batch().insert( ModelPosition.createFromParentAndOffset( root, 0 ), insertedText ); + doc.batch().insertText( 'foo', { key: 'value' }, root ); expect( cbInsertText.called ).to.be.true; expect( cbAddAttribute.called ).to.be.true; @@ -129,18 +128,21 @@ describe( 'ModelConversionDispatcher', () => { it( 'should fire changeAttribute callbacks for change attribute change', () => { const cbChangeText = sinon.spy(); const cbChangeImage = sinon.spy(); + const batch = doc.batch(); dispatcher.on( 'changeAttribute:key:$text', cbChangeText ); dispatcher.on( 'changeAttribute:key:image', cbChangeImage ); - doc.batch().setAttribute( image, 'key', 'value' ).setAttribute( image, 'key', 'newValue' ); + batch.setAttribute( image, 'key', 'value' ); + batch.setAttribute( image, 'key', 'newValue' ); // Callback for adding attribute on text not called. expect( cbChangeText.called ).to.be.false; expect( cbChangeImage.calledOnce ).to.be.true; const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ); - doc.batch().setAttribute( range, 'key', 'value' ).setAttribute( range, 'key', 'newValue' ); + batch.setAttribute( range, 'key', 'value' ); + batch.setAttribute( range, 'key', 'newValue' ); expect( cbChangeText.calledOnce ).to.be.true; // Callback for adding attribute on image not called this time. @@ -150,18 +152,21 @@ describe( 'ModelConversionDispatcher', () => { it( 'should fire removeAttribute callbacks for remove attribute change', () => { const cbRemoveText = sinon.spy(); const cbRemoveImage = sinon.spy(); + const batch = doc.batch(); dispatcher.on( 'removeAttribute:key:$text', cbRemoveText ); dispatcher.on( 'removeAttribute:key:image', cbRemoveImage ); - doc.batch().setAttribute( image, 'key', 'value' ).removeAttribute( image, 'key' ); + batch.setAttribute( image, 'key', 'value' ); + batch.removeAttribute( image, 'key' ); // Callback for adding attribute on text not called. expect( cbRemoveText.called ).to.be.false; expect( cbRemoveImage.calledOnce ).to.be.true; const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ); - doc.batch().setAttribute( range, 'key', 'value' ).removeAttribute( range, 'key' ); + batch.setAttribute( range, 'key', 'value' ); + batch.removeAttribute( range, 'key' ); expect( cbRemoveText.calledOnce ).to.be.true; // Callback for adding attribute on image not called this time. @@ -616,9 +621,10 @@ describe( 'ModelConversionDispatcher', () => { it( 'should prepare correct list of consumable values', () => { doc.enqueueChanges( () => { - doc.batch() - .setAttribute( ModelRange.createIn( root ), 'bold', true ) - .setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); + const batch = doc.batch(); + + batch.setAttribute( ModelRange.createIn( root ), 'bold', true ); + batch.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); } ); dispatcher.on( 'selection', ( evt, data, consumable ) => { @@ -634,9 +640,10 @@ describe( 'ModelConversionDispatcher', () => { sinon.spy( dispatcher, 'fire' ); doc.enqueueChanges( () => { - doc.batch() - .setAttribute( ModelRange.createIn( root ), 'bold', true ) - .setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); + const batch = doc.batch(); + + batch.setAttribute( ModelRange.createIn( root ), 'bold', true ); + batch.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); } ); dispatcher.convertSelection( doc.selection, [] ); @@ -653,9 +660,10 @@ describe( 'ModelConversionDispatcher', () => { } ); doc.enqueueChanges( () => { - doc.batch() - .setAttribute( ModelRange.createIn( root ), 'bold', true ) - .setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); + const batch = doc.batch(); + + batch.setAttribute( ModelRange.createIn( root ), 'bold', true ); + batch.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); } ); dispatcher.convertSelection( doc.selection, [] ); diff --git a/tests/model/documentselection.js b/tests/model/documentselection.js index a5a2e872c..4a8d490fc 100644 --- a/tests/model/documentselection.js +++ b/tests/model/documentselection.js @@ -1012,7 +1012,7 @@ describe( 'DocumentSelection', () => { batchTypes.set( batch, batch.type ); } ); - doc.batch().insert( rangeInEmptyP.start, 'x' ); + doc.batch().insertText( 'x', rangeInEmptyP.start ); expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false; expect( emptyP.hasAttribute( abcStoreAttrKey ) ).to.be.false; @@ -1024,7 +1024,7 @@ describe( 'DocumentSelection', () => { selection.setRanges( [ rangeInEmptyP ] ); selection.setAttribute( 'foo', 'bar' ); - doc.batch().move( fullP.getChild( 0 ), rangeInEmptyP.start ); + doc.batch().move( Range.createOn( fullP.getChild( 0 ) ), rangeInEmptyP.start ); expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false; } ); @@ -1048,7 +1048,7 @@ describe( 'DocumentSelection', () => { selection.setRanges( [ rangeInFullP ] ); - doc.batch().insert( rangeInEmptyP.start, 'x' ); + doc.batch().insertText( 'x', rangeInEmptyP.start ); expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.false; } ); @@ -1076,7 +1076,7 @@ describe( 'DocumentSelection', () => { selection.setAttribute( 'foo', 'bar' ); doc.enqueueChanges( () => { - doc.batch().insert( rangeInEmptyP.start, 'x' ); + doc.batch().insertText( 'x', rangeInEmptyP.start ); // `emptyP` still has the attribute, because attribute clearing is in enqueued block. expect( emptyP.hasAttribute( fooStoreAttrKey ) ).to.be.true; @@ -1109,7 +1109,7 @@ describe( 'DocumentSelection', () => { sinon.spy( doc, 'enqueueChanges' ); - doc.batch( 'transparent' ).insert( rangeInEmptyP.start, 'x' ); + doc.batch( 'transparent' ).insertText( 'x', rangeInEmptyP.start ); expect( doc.enqueueChanges.called ).to.be.false; expect( emptyP.getAttribute( fooStoreAttrKey ) ).to.equal( 'bar' ); diff --git a/tests/model/markercollection.js b/tests/model/markercollection.js index 5b9f3e24f..a047813b5 100644 --- a/tests/model/markercollection.js +++ b/tests/model/markercollection.js @@ -225,7 +225,7 @@ describe( 'Marker', () => { expect( marker.getEnd().isEqual( range.end ) ).to.be.true; doc.enqueueChanges( () => { - doc.batch().insert( Position.createAt( root, 0 ), 'abc' ); + doc.batch().insertText( 'abc', root ); } ); const updatedRange = Range.createFromParentsAndOffsets( root, 4, root, 5 ); From f10c1a140eb5980eb9cbf36809dc767f3e32314a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 21:38:12 +0100 Subject: [PATCH 26/44] Refactored skipped Batch#setMarker tests. --- tests/model/batch.js | 57 +++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/tests/model/batch.js b/tests/model/batch.js index aa83dc112..eb969a117 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -9,8 +9,6 @@ import InsertDelta from '../../src/model/delta/insertdelta'; import WeakInsertDelta from '../../src/model/delta/weakinsertdelta'; import Operation from '../../src/model/operation/operation'; -import InsertOperation from '../../src/model/operation/insertoperation'; -import MarkerOperation from '../../src/model/operation/markeroperation'; import Document from '../../src/model/document'; import DocumentFragment from '../../src/model/documentfragment'; @@ -22,7 +20,6 @@ import Range from '../../src/model/range'; import count from '@ckeditor/ckeditor5-utils/src/count'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; -import { stringify } from '../../src/dev-utils/model'; import { getNodesAndText } from '../../tests/model/_utils/utils'; describe( 'Batch', () => { @@ -380,39 +377,51 @@ describe( 'Batch', () => { expect( spy.secondCall.args[ 0 ].delta.batch ).to.equal( batch ); } ); - it.skip( 'should transfer markers from given DocumentFragment', () => { - const documentFragment = batch.createDocumentFragment(); - const li = batch.createElement( 'li' ); + it( 'should transfer markers from given DocumentFragment', () => { + const root = doc.createRoot(); + const docFrag = batch.createDocumentFragment(); - batch.insert( batch.createText( 'foo bar' ), li ); - batch.insert( li, documentFragment ); + batch.appendText( 'abcd', root ); + batch.appendElement( 'p', docFrag ); + batch.insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) ); - const marker = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 5 ] ) ); + const marker = new Range( new Position( docFrag, [ 0, 1 ] ), new Position( docFrag, [ 0, 5 ] ) ); - documentFragment.markers.set( 'marker', marker ); + docFrag.markers.set( 'marker', marker ); - batch.insert( documentFragment, new Position( root, [ 3, 0 ] ) ); + batch.insert( docFrag, new Position( root, [ 2 ] ) ); expect( Array.from( doc.markers ).length ).to.equal( 1 ); - expect( stringify( root, doc.markers.get( 'marker' ).getRange() ) ).to.equal( 'ab

  • f[oo b]ar
c' ); + + const range = doc.markers.get( 'marker' ).getRange(); + expect( range.root ).to.equal( root ); + expect( range.start.path ).to.deep.equal( [ 2, 1 ] ); + expect( range.end.path ).to.deep.equal( [ 2, 5 ] ); } ); - it.skip( 'should set each marker as separate operation', () => { - sinon.spy( doc, 'applyOperation' ); + it( 'should set each marker as a separate operation', () => { + const spy = sinon.spy(); + const root = doc.createRoot(); + const docFrag = batch.createDocumentFragment(); + + batch.appendText( 'abcd', root ); + batch.appendElement( 'p', docFrag ); + batch.insertText( 'foo bar', new Position( docFrag, [ 0, 0 ] ) ); + + const marker1 = new Range( new Position( docFrag, [ 0, 1 ] ), new Position( docFrag, [ 0, 2 ] ) ); + const marker2 = new Range( new Position( docFrag, [ 0, 5 ] ), new Position( docFrag, [ 0, 6 ] ) ); - const documentFragment = new DocumentFragment( [ new Element( 'li', null, [ new Text( 'foo bar' ) ] ) ] ); - const marker1 = new Range( new Position( documentFragment, [ 0, 1 ] ), new Position( documentFragment, [ 0, 2 ] ) ); - const marker2 = new Range( new Position( documentFragment, [ 0, 5 ] ), new Position( documentFragment, [ 0, 6 ] ) ); + docFrag.markers.set( 'marker1', marker1 ); + docFrag.markers.set( 'marker2', marker2 ); - documentFragment.markers.set( 'marker1', marker1 ); - documentFragment.markers.set( 'marker2', marker2 ); + doc.on( 'change', spy ); - batch.insert( new Position( root, [ 3, 0 ] ), documentFragment ); + batch.insert( docFrag, new Position( root, [ 2 ] ) ); - expect( doc.applyOperation.calledThrice ); - expect( doc.applyOperation.firstCall.calledWith( sinon.match( operation => operation instanceof InsertOperation ) ) ); - expect( doc.applyOperation.secondCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); - expect( doc.applyOperation.thirdCall.calledWith( sinon.match( operation => operation instanceof MarkerOperation ) ) ); + sinon.assert.calledThrice( spy ); + expect( spy.firstCall.args[ 1 ] ).to.equal( 'insert' ); + expect( spy.secondCall.args[ 1 ] ).to.equal( 'marker' ); + expect( spy.thirdCall.args[ 1 ] ).to.equal( 'marker' ); } ); } ); From 7af892885afaa19fd7d4bfaab8c2a84971f124fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 23 Nov 2017 21:51:40 +0100 Subject: [PATCH 27/44] Improved adding attributes to detached root. --- src/model/batch.js | 4 ++-- tests/model/batch.js | 50 ++++++++++++++++++++------------------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index b643896ba..b1b2ab7bb 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -736,10 +736,10 @@ function setAttributeToItem( batch, key, value, item ) { let range, operation; if ( previousValue != value ) { - const delta = item.is( 'rootElement' ) ? new RootAttributeDelta() : new AttributeDelta(); + const delta = item.root === item ? new RootAttributeDelta() : new AttributeDelta(); batch.addDelta( delta ); - if ( item.is( 'rootElement' ) ) { + 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 { diff --git a/tests/model/batch.js b/tests/model/batch.js index eb969a117..1fecf9bb4 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -934,10 +934,6 @@ describe( 'Batch', () => { describe( 'setAttribute() / removeAttribute()', () => { let batch, doc, root, spy; - const correctDeltaMatcher = sinon.match( operation => { - return operation.delta && operation.delta.batch && operation.delta.batch == batch; - } ); - beforeEach( () => { doc = new Document(); root = doc.createRoot(); @@ -987,12 +983,6 @@ describe( 'Batch', () => { expect( spy.callCount ).to.equal( 0 ); expect( node.getAttribute( 'a' ) ).to.equal( 1 ); } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - batch.setAttribute( node, 'b', 2 ); - - sinon.assert.calledWith( spy, correctDeltaMatcher ); - } ); } ); describe( 'removeAttribute', () => { @@ -1012,12 +1002,6 @@ describe( 'Batch', () => { batch.removeAttribute( node, 'b' ); expect( spy.callCount ).to.equal( 0 ); } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - batch.removeAttribute( node, 'a' ); - - sinon.assert.calledWith( spy, correctDeltaMatcher ); - } ); } ); } ); @@ -1157,12 +1141,6 @@ describe( 'Batch', () => { expect( getChangesAttrsCount() ).to.equal( 14 ); expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' ); } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); - - expect( doc.applyOperation.calledWith( correctDeltaMatcher ) ).to.be.true; - } ); } ); describe( 'removeAttribute', () => { @@ -1238,16 +1216,14 @@ describe( 'Batch', () => { expect( getChangesAttrsCount() ).to.equal( 6 ); expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); } ); - - it( 'should add delta to batch and operation to delta before applying operation', () => { - batch.removeAttribute( getRange( 0, 2 ), 'a' ); - sinon.assert.calledWith( spy, correctDeltaMatcher ); - } ); } ); } ); describe( 'change attribute on root element', () => { + let p; + beforeEach( () => { + p = batch.createElement( 'p', { a: 3 } ); spy = sinon.spy( doc, 'applyOperation' ); } ); @@ -1258,12 +1234,24 @@ describe( 'Batch', () => { expect( root.getAttribute( 'b' ) ).to.equal( 2 ); } ); + it( 'should create the attribute on detached root', () => { + batch.setAttribute( p, 'b', 2 ); + expect( spy.callCount ).to.equal( 1 ); + expect( p.getAttribute( 'b' ) ).to.equal( 2 ); + } ); + it( 'should change the attribute of root', () => { batch.setAttribute( root, 'a', 2 ); expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'a' ) ).to.equal( 2 ); } ); + it( 'should change the attribute of detached root', () => { + batch.setAttribute( p, 'a', 2 ); + expect( spy.callCount ).to.equal( 1 ); + expect( p.getAttribute( 'a' ) ).to.equal( 2 ); + } ); + it( 'should do nothing if the attribute value is the same', () => { batch.setAttribute( root, 'a', 1 ); expect( spy.callCount ).to.equal( 1 ); @@ -1271,6 +1259,14 @@ describe( 'Batch', () => { expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'a' ) ).to.equal( 1 ); } ); + + it( 'should do nothing if the attribute value is the same on detached root', () => { + batch.setAttribute( p, 'a', 1 ); + expect( spy.callCount ).to.equal( 1 ); + batch.setAttribute( p, 'a', 1 ); + expect( spy.callCount ).to.equal( 1 ); + expect( p.getAttribute( 'a' ) ).to.equal( 1 ); + } ); } ); describe( 'removeAttribute', () => { From 92ec864cc2f50e9c5ad6846942f47201796079d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 24 Nov 2017 16:00:01 +0100 Subject: [PATCH 28/44] Extracted RootAttributeDelta to the separate file. --- src/model/batch.js | 3 ++- src/model/delta/attributedelta.js | 18 --------------- src/model/delta/rootattributedelta.js | 30 +++++++++++++++++++++++++ tests/model/delta/attributedelta.js | 8 +------ tests/model/delta/rootattributedelta.js | 12 ++++++++++ 5 files changed, 45 insertions(+), 26 deletions(-) create mode 100644 src/model/delta/rootattributedelta.js create mode 100644 tests/model/delta/rootattributedelta.js diff --git a/src/model/batch.js b/src/model/batch.js index b1b2ab7bb..7fc509cce 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -7,13 +7,14 @@ * @module engine/model/batch */ -import { default as AttributeDelta, RootAttributeDelta } from './delta/attributedelta'; +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'; diff --git a/src/model/delta/attributedelta.js b/src/model/delta/attributedelta.js index fa27e2256..2be997eef 100644 --- a/src/model/delta/attributedelta.js +++ b/src/model/delta/attributedelta.js @@ -108,22 +108,4 @@ export default class AttributeDelta extends Delta { } } -/** - * To provide specific OT behavior and better collisions solving, methods to change attributes - * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute}) - * use `RootAttributeDelta` class which inherits from the `Delta` class and may - * overwrite some methods. - * - * @extends module:engine/model/delta/delta~Delta - */ -export class RootAttributeDelta extends Delta { - /** - * @inheritDoc - */ - static get className() { - return 'engine.model.delta.RootAttributeDelta'; - } -} - DeltaFactory.register( AttributeDelta ); -DeltaFactory.register( RootAttributeDelta ); diff --git a/src/model/delta/rootattributedelta.js b/src/model/delta/rootattributedelta.js new file mode 100644 index 000000000..004fc2b2d --- /dev/null +++ b/src/model/delta/rootattributedelta.js @@ -0,0 +1,30 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module engine/model/delta/rootattributedelta + */ + +import Delta from './delta'; +import DeltaFactory from './deltafactory'; + +/** + * To provide specific OT behavior and better collisions solving, methods to change attributes + * ({@link module:engine/model/batch~Batch#setAttribute} and {@link module:engine/model/batch~Batch#removeAttribute}) + * use `RootAttributeDelta` class which inherits from the `Delta` class and may + * overwrite some methods. + * + * @extends module:engine/model/delta/delta~Delta + */ +export default class RootAttributeDelta extends Delta { + /** + * @inheritDoc + */ + static get className() { + return 'engine.model.delta.RootAttributeDelta'; + } +} + +DeltaFactory.register( RootAttributeDelta ); diff --git a/tests/model/delta/attributedelta.js b/tests/model/delta/attributedelta.js index 442a17a4c..d1eb4a88f 100644 --- a/tests/model/delta/attributedelta.js +++ b/tests/model/delta/attributedelta.js @@ -6,7 +6,7 @@ import Document from '../../../src/model/document'; import Range from '../../../src/model/range'; import Position from '../../../src/model/position'; -import { default as AttributeDelta, RootAttributeDelta } from '../../../src/model/delta/attributedelta'; +import AttributeDelta from '../../../src/model/delta/attributedelta'; import AttributeOperation from '../../../src/model/operation/attributeoperation'; import NoOperation from '../../../src/model/operation/nooperation'; import { jsonParseStringify } from '../../../tests/model/_utils/utils'; @@ -128,9 +128,3 @@ describe( 'AttributeDelta', () => { expect( json ).not.to.have.property( '_range' ); } ); } ); - -describe( 'RootAttributeDelta', () => { - it( 'should provide proper className', () => { - expect( RootAttributeDelta.className ).to.equal( 'engine.model.delta.RootAttributeDelta' ); - } ); -} ); diff --git a/tests/model/delta/rootattributedelta.js b/tests/model/delta/rootattributedelta.js new file mode 100644 index 000000000..1061be0d5 --- /dev/null +++ b/tests/model/delta/rootattributedelta.js @@ -0,0 +1,12 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import RootAttributeDelta from '../../../src/model/delta/rootattributedelta'; + +describe( 'RootAttributeDelta', () => { + it( 'should provide proper className', () => { + expect( RootAttributeDelta.className ).to.equal( 'engine.model.delta.RootAttributeDelta' ); + } ); +} ); From 59c4dca0743f542a5a9b87366d06671f37d83979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 24 Nov 2017 17:52:14 +0100 Subject: [PATCH 29/44] Used position and offset in DetachOperation. --- src/model/operation/detachoperation.js | 34 ++++++++++++++++++------ tests/model/operation/detachoperation.js | 10 +++---- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/model/operation/detachoperation.js b/src/model/operation/detachoperation.js index 1fe8adaf3..c6a654a70 100644 --- a/src/model/operation/detachoperation.js +++ b/src/model/operation/detachoperation.js @@ -8,6 +8,8 @@ */ import Operation from './operation'; +import Position from '../position'; +import Range from '../range'; import { remove } from '../writer'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; @@ -21,19 +23,28 @@ export default class DetachOperation extends Operation { /** * Creates an insert operation. * - * @param {module:engine/model/range~Range} range Range to remove. + * @param {module:engine/model/position~Position} sourcePosition + * Position before the first {@link module:engine/model/item~Item model item} to move. + * @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at + * `sourcePosition` with offset shifted by `howMany`. * @param {Number} baseVersion {@link module:engine/model/document~Document#version} on which operation can be applied. */ - constructor( range, baseVersion ) { + constructor( sourcePosition, howMany, baseVersion ) { super( baseVersion ); /** - * Node to remove. + * Position before the first {@link module:engine/model/item~Item model item} to detach. * - * @readonly - * @member {module:engine/model/range~Range} #range + * @member {module:engine/model/position~Position} #sourcePosition */ - this.range = range; + this.sourcePosition = Position.createFromPosition( sourcePosition ); + + /** + * Offset size of moved range. + * + * @member {Number} #howMany + */ + this.howMany = howMany; } /** @@ -54,7 +65,7 @@ export default class DetachOperation extends Operation { * @inheritDoc */ _execute() { - if ( this.range.root.document ) { + if ( this.sourcePosition.root.document ) { /** * Cannot detach document node. * Use {@link module:engine/model/operation/removeoperation~RemoveOperation remove operation} instead. @@ -64,8 +75,15 @@ export default class DetachOperation extends Operation { throw new CKEditorError( 'detach-operation-on-document-node: Cannot detach document node.' ); } - const nodes = remove( this.range ); + const nodes = remove( Range.createFromPositionAndShift( this.sourcePosition, this.howMany ) ); return { nodes }; } + + /** + * @inheritDoc + */ + static get className() { + return 'engine.model.operation.DetachOperation'; + } } diff --git a/tests/model/operation/detachoperation.js b/tests/model/operation/detachoperation.js index 95ac17b2a..6d1a16856 100644 --- a/tests/model/operation/detachoperation.js +++ b/tests/model/operation/detachoperation.js @@ -7,7 +7,7 @@ import Document from '../../../src/model/document'; import DetachOperation from '../../../src/model/operation/detachoperation'; import { wrapInDelta } from '../../../tests/model/_utils/utils'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; -import Range from '../../../src/model/range'; +import Position from '../../../src/model/position'; describe( 'DetachOperation', () => { let doc, batch, docFrag, element; @@ -22,13 +22,13 @@ describe( 'DetachOperation', () => { } ); it( 'should have type equal to detach', () => { - const op = new DetachOperation( element, doc.version ); + const op = new DetachOperation( Position.createBefore( element ), 1, doc.version ); expect( op.type ).to.equal( 'detach' ); } ); it( 'should remove given element from parent', () => { - const op = new DetachOperation( Range.createOn( element ), doc.version ); + const op = new DetachOperation( Position.createBefore( element ), 1, doc.version ); doc.applyOperation( wrapInDelta( op ) ); @@ -40,7 +40,7 @@ describe( 'DetachOperation', () => { const element = batch.createElement( 'element' ); batch.append( element, root ); - const op = new DetachOperation( Range.createOn( element ), doc.version ); + const op = new DetachOperation( Position.createBefore( element ), 1, doc.version ); expect( () => { op._execute(); @@ -48,7 +48,7 @@ describe( 'DetachOperation', () => { } ); it( 'should be not a document operation', () => { - const op = new DetachOperation( element, doc.version ); + const op = new DetachOperation( Position.createBefore( element ), 1, doc.version ); expect( op.isDocumentOperation ).to.false; } ); From bfe84dcc7218912478a9901a99144d2e855fa88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 24 Nov 2017 17:53:22 +0100 Subject: [PATCH 30/44] Added DetachOperation to EngineDebug. --- src/dev-utils/enableenginedebug.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/dev-utils/enableenginedebug.js b/src/dev-utils/enableenginedebug.js index 24cabc8d9..2861428e8 100644 --- a/src/dev-utils/enableenginedebug.js +++ b/src/dev-utils/enableenginedebug.js @@ -18,6 +18,7 @@ import ModelTextProxy from '../model/textproxy'; import ModelElement from '../model/element'; import Operation from '../model/operation/operation'; import AttributeOperation from '../model/operation/attributeoperation'; +import DetachOperation from '../model/operation/detachoperation'; import InsertOperation from '../model/operation/insertoperation'; import MarkerOperation from '../model/operation/markeroperation'; import MoveOperation from '../model/operation/moveoperation'; @@ -25,7 +26,8 @@ import NoOperation from '../model/operation/nooperation'; import RenameOperation from '../model/operation/renameoperation'; import RootAttributeOperation from '../model/operation/rootattributeoperation'; import Delta from '../model/delta/delta'; -import { default as AttributeDelta, RootAttributeDelta } from '../model/delta/attributedelta'; +import AttributeDelta from '../model/delta/attributedelta'; +import RootAttributeDelta from '../model/delta/rootattributedelta'; import InsertDelta from '../model/delta/insertdelta'; import MarkerDelta from '../model/delta/markerdelta'; import MergeDelta from '../model/delta/mergedelta'; @@ -273,6 +275,13 @@ function enableLoggingTools() { `"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`; }; + DetachOperation.prototype.toString = function() { + const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany ); + const nodes = Array.from( range.getItems() ); + + return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodes.length > 1 ? range : nodes[ 0 ] + ' ' + range }`; + }; + InsertOperation.prototype.toString = function() { const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 ); From ff4ff7d332ecbbe05407428b108d9e14b516564f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 24 Nov 2017 17:54:00 +0100 Subject: [PATCH 31/44] Aligned Batch#remove with new DetachOperation API. --- src/model/batch.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index 7fc509cce..1b2149089 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -339,9 +339,7 @@ export default class Batch { operation = new RemoveOperation( position, howMany, gyPosition, this.document.version ); } else { - const range = Range.createFromPositionAndShift( position, howMany ); - - operation = new DetachOperation( range, this.document.version ); + operation = new DetachOperation( position, howMany, this.document.version ); } delta.addOperation( operation ); From c587d71c7c441005181a4db986ed0f53cbe2bcc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Fri, 24 Nov 2017 17:56:19 +0100 Subject: [PATCH 32/44] Fixed invalid import path. --- src/dev-utils/enableenginedebug.js | 2 +- tests/dev-utils/enableenginedebug.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dev-utils/enableenginedebug.js b/src/dev-utils/enableenginedebug.js index 2861428e8..a2644e691 100644 --- a/src/dev-utils/enableenginedebug.js +++ b/src/dev-utils/enableenginedebug.js @@ -27,12 +27,12 @@ import RenameOperation from '../model/operation/renameoperation'; import RootAttributeOperation from '../model/operation/rootattributeoperation'; import Delta from '../model/delta/delta'; import AttributeDelta from '../model/delta/attributedelta'; -import RootAttributeDelta from '../model/delta/rootattributedelta'; import InsertDelta from '../model/delta/insertdelta'; import MarkerDelta from '../model/delta/markerdelta'; import MergeDelta from '../model/delta/mergedelta'; import MoveDelta from '../model/delta/movedelta'; import RenameDelta from '../model/delta/renamedelta'; +import RootAttributeDelta from '../model/delta/rootattributedelta'; import SplitDelta from '../model/delta/splitdelta'; import UnwrapDelta from '../model/delta/unwrapdelta'; import WrapDelta from '../model/delta/wrapdelta'; diff --git a/tests/dev-utils/enableenginedebug.js b/tests/dev-utils/enableenginedebug.js index dc4a3323d..d52e9854e 100644 --- a/tests/dev-utils/enableenginedebug.js +++ b/tests/dev-utils/enableenginedebug.js @@ -22,12 +22,13 @@ import RootAttributeOperation from '../../src/model/operation/rootattributeopera import RemoveOperation from '../../src/model/operation/removeoperation'; import DeltaFactory from '../../src/model/delta/deltafactory'; import Delta from '../../src/model/delta/delta'; -import { default as AttributeDelta, RootAttributeDelta } from '../../src/model/delta/attributedelta'; +import AttributeDelta from '../../src/model/delta/attributedelta'; import InsertDelta from '../../src/model/delta/insertdelta'; import MarkerDelta from '../../src/model/delta/markerdelta'; import MergeDelta from '../../src/model/delta/mergedelta'; import MoveDelta from '../../src/model/delta/movedelta'; import RenameDelta from '../../src/model/delta/renamedelta'; +import RootAttributeDelta from '../../src/model/delta/rootattributedelta'; import SplitDelta from '../../src/model/delta/splitdelta'; import UnwrapDelta from '../../src/model/delta/unwrapdelta'; import WrapDelta from '../../src/model/delta/wrapdelta'; From 2efb4145d0cea1b7e6823faea001299ae9400ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 27 Nov 2017 12:21:00 +0100 Subject: [PATCH 33/44] Increased CC of enginedebug. --- src/dev-utils/enableenginedebug.js | 3 ++- tests/dev-utils/enableenginedebug.js | 34 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/dev-utils/enableenginedebug.js b/src/dev-utils/enableenginedebug.js index a2644e691..e042f26a7 100644 --- a/src/dev-utils/enableenginedebug.js +++ b/src/dev-utils/enableenginedebug.js @@ -278,8 +278,9 @@ function enableLoggingTools() { DetachOperation.prototype.toString = function() { const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany ); const nodes = Array.from( range.getItems() ); + const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ]; - return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodes.length > 1 ? range : nodes[ 0 ] + ' ' + range }`; + return getClassName( this ) + `( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`; }; InsertOperation.prototype.toString = function() { diff --git a/tests/dev-utils/enableenginedebug.js b/tests/dev-utils/enableenginedebug.js index d52e9854e..e15857f69 100644 --- a/tests/dev-utils/enableenginedebug.js +++ b/tests/dev-utils/enableenginedebug.js @@ -13,6 +13,7 @@ import ModelText from '../../src/model/text'; import ModelTextProxy from '../../src/model/textproxy'; import ModelElement from '../../src/model/element'; import AttributeOperation from '../../src/model/operation/attributeoperation'; +import DetachOperation from '../../src/model/operation/detachoperation'; import InsertOperation from '../../src/model/operation/insertoperation'; import MarkerOperation from '../../src/model/operation/markeroperation'; import MoveOperation from '../../src/model/operation/moveoperation'; @@ -210,6 +211,39 @@ describe( 'debug tools', () => { expect( log.calledWithExactly( op.toString() ) ).to.be.true; } ); + it( 'DetachOperation (text node)', () => { + const op = new DetachOperation( ModelPosition.createAt( modelRoot, 0 ), 3, 0 ); + + expect( op.toString() ).to.equal( 'DetachOperation( 0 ): #foo -> main [ 0 ] - [ 3 ]' ); + + op.log(); + expect( log.calledWithExactly( op.toString() ) ).to.be.true; + } ); + + it( 'DetachOperation (element)', () => { + const element = new ModelElement( 'element' ); + modelRoot.insertChildren( 0, element ); + + const op = new DetachOperation( ModelPosition.createBefore( element ), 1, 0 ); + + expect( op.toString() ).to.equal( 'DetachOperation( 0 ): -> main [ 0 ] - [ 1 ]' ); + + op.log(); + expect( log.calledWithExactly( op.toString() ) ).to.be.true; + } ); + + it( 'DetachOperation (multiple nodes)', () => { + const element = new ModelElement( 'element' ); + modelRoot.insertChildren( 0, element ); + + const op = new DetachOperation( ModelPosition.createBefore( element ), 2, 0 ); + + expect( op.toString() ).to.equal( 'DetachOperation( 0 ): [ 2 ] -> main [ 0 ] - [ 2 ]' ); + + op.log(); + expect( log.calledWithExactly( op.toString() ) ).to.be.true; + } ); + it( 'InsertOperation (text node)', () => { const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 ); From cf582fb50961b9d3ba2581d307f8c9d217e6ea2a Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Mon, 27 Nov 2017 13:43:22 +0100 Subject: [PATCH 34/44] Tests: add missing tests for clearAttributes. --- tests/model/batch.js | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/model/batch.js b/tests/model/batch.js index 1fecf9bb4..1c482736d 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -1283,6 +1283,61 @@ describe( 'Batch', () => { expect( spy.callCount ).to.equal( 0 ); } ); } ); + + describe( 'clearAttributes', () => { + it( 'should clear attributes from range', () => { + batch.appendText( 'xxx', { a: 1, b: 2, c: 3 }, root ); + batch.appendText( 'xxx', root ); + batch.appendText( 'xxx', { a: 1 }, root ); + batch.appendText( 'xxx', { b: 2 }, root ); + batch.appendText( 'xxx', root ); + batch.appendElement( 'e', { a: 1 }, root ); + batch.appendText( 'xxx', root ); + + const range = Range.createIn( root ); + + batch.clearAttributes( range ); + + let itemsCount = 0; + + for ( const item of range.getItems() ) { + itemsCount++; + expect( Array.from( item.getAttributeKeys() ).length ).to.equal( 0 ); + } + + expect( itemsCount ).to.equal( 3 ); + } ); + + it( 'should clear attributes on element', () => { + const element = batch.createElement( 'x', { a: 1, b: 2, c: 3 }, root ); + + expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 3 ); + + batch.clearAttributes( element ); + + expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 ); + } ); + + it( 'should clear attributes on root element', () => { + batch.setAttributes( root, { a: 1, b: 2, c: 3 } ); + + expect( Array.from( root.getAttributeKeys() ).length ).to.equal( 3 ); + + batch.clearAttributes( root ); + + expect( Array.from( root.getAttributeKeys() ).length ).to.equal( 0 ); + } ); + + it( 'should do nothing if there are no attributes', () => { + const element = batch.createElement( 'x' ); + + expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 ); + + batch.clearAttributes( element ); + + expect( Array.from( element.getAttributeKeys() ).length ).to.equal( 0 ); + } ); + } ); } ); it( 'should not add empty delta to the batch', () => { From 4334b642a62cfc8d7c45e81f32314738fc0ae2dc Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Mon, 27 Nov 2017 15:13:42 +0100 Subject: [PATCH 35/44] Tests: added tests for split element with no parent and document fragment. Docs: Remove @chainable from methods which are not chainable anymore. --- src/model/batch.js | 25 ++++++------------------- tests/model/batch.js | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index 1b2149089..17492067e 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -56,9 +56,6 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; * batch.insert( firstPosition, 'foo' ); * batch.insert( secondPosition, 'bar' ); * - * Note that all document modification methods (insert, remove, split, etc.) are chainable so you can shorten code to: - * - * doc.batch().insert( firstPosition, 'foo' ).insert( secondPosition, 'bar' ); */ export default class Batch { /** @@ -228,7 +225,6 @@ export default class Batch { * 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}. * - * @chainable * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange * Model item or range on which the attribute will be set. * @param {String} key Attribute key. @@ -252,7 +248,6 @@ export default class Batch { * 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}. * - * @chainable * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange * Model item or range from which the attribute will be removed. * @method module:engine/model/batch~Batch#removeAttribute @@ -322,9 +317,8 @@ export default class Batch { } /** - * Removes given {@link module:engine/model/item~Item model item} or given range. + * Removes given model {@link module:engine/model/item~Item item} or {@link module:engine/model/range~Range range}. * - * @chainable * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange Model item or range to remove. */ remove( itemOrRange ) { @@ -366,7 +360,6 @@ export default class Batch { * 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. * - * @chainable * @param {module:engine/model/position~Position} position Position of merge. */ merge( position ) { @@ -419,7 +412,6 @@ export default class Batch { /** * Renames given element. * - * @chainable * @param {module:engine/model/element~Element} element The element to rename. * @param {String} newName New element name. */ @@ -444,10 +436,9 @@ export default class Batch { /** * Splits an element at the given position. * - * The element cannot be a root element, as root element cannot be split. The `batch-split-root` error will be thrown if - * you try to split the root element. + * 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. * - * @chainable * @param {module:engine/model/position~Position} position Position of split. */ split( position ) { @@ -458,11 +449,11 @@ export default class Batch { if ( !splitElement.parent ) { /** - * Root element can not be split. + * Element with no parent can not be split. * - * @error batch-split-root + * @error batch-split-element-no-parent */ - throw new CKEditorError( 'batch-split-root: Root element can not be split.' ); + throw new CKEditorError( 'batch-split-element-no-parent: Element with no parent can not be split.' ); } const copy = new Element( splitElement.name, splitElement.getAttributes() ); @@ -492,7 +483,6 @@ export default class Batch { * 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. * - * @chainable * @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. */ @@ -548,7 +538,6 @@ export default class Batch { * 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. * - * @chainable * @param {module:engine/model/element~Element} element Element to unwrap. */ unwrap( element ) { @@ -600,7 +589,6 @@ export default class Batch { * 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. * - * @chainable * @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. */ @@ -632,7 +620,6 @@ export default class Batch { /** * Removes given {@link module:engine/model/markercollection~Marker marker} or marker with given name. * - * @chainable * @param {module:engine/model/markercollection~Marker|String} markerOrName Marker or marker name to remove. */ removeMarker( markerOrName ) { diff --git a/tests/model/batch.js b/tests/model/batch.js index 1c482736d..9fc9c49f1 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -1729,7 +1729,27 @@ describe( 'Batch', () => { it( 'should throw if we try to split a root', () => { expect( () => { doc.batch().split( new Position( root, [ 0 ] ) ); - } ).to.throw( CKEditorError, /^batch-split-root/ ); + } ).to.throw( CKEditorError, /^batch-split-element-no-parent/ ); + } ); + + it( 'should throw if we try to split an element with no parent', () => { + const batch = doc.batch(); + + expect( () => { + const element = batch.createElement( 'p' ); + + batch.split( new Position( element, [ 0 ] ) ); + } ).to.throw( CKEditorError, /^batch-split-element-no-parent/ ); + } ); + + it( 'should throw if we try to split a document fragment', () => { + const batch = doc.batch(); + + expect( () => { + const documentFragment = batch.createDocumentFragment(); + + batch.split( new Position( documentFragment, [ 0 ] ) ); + } ).to.throw( CKEditorError, /^batch-split-element-no-parent/ ); } ); } ); From 3c88520131416893ab9c479f13c07aeecf1205b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 27 Nov 2017 17:21:51 +0100 Subject: [PATCH 36/44] Refactored conversion and DataController to use new Batch API. --- src/controller/datacontroller.js | 12 ++-- src/controller/insertcontent.js | 6 +- src/conversion/buildviewconverter.js | 21 +++--- src/conversion/view-to-model-converters.js | 10 +-- src/conversion/viewconversiondispatcher.js | 56 ++++++++------- src/dev-utils/model.js | 13 ++-- src/model/document.js | 3 +- src/model/schema.js | 13 +--- tests/controller/datacontroller.js | 36 ++++++---- tests/controller/editingcontroller.js | 9 ++- tests/controller/insertcontent.js | 6 +- tests/conversion/advanced-converters.js | 39 +++++------ tests/conversion/buildviewconverter.js | 71 +++++++++++--------- tests/conversion/view-to-model-converters.js | 22 +++--- tests/conversion/viewconversiondispatcher.js | 39 ++++++----- tests/dev-utils/model.js | 24 +++---- tests/model/document/document.js | 8 +-- tests/model/schema/schema.js | 31 ++------- tests/model/selection.js | 8 +-- 19 files changed, 214 insertions(+), 213 deletions(-) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index 1bb558dfa..7443f4c17 100644 --- a/src/controller/datacontroller.js +++ b/src/controller/datacontroller.js @@ -198,7 +198,7 @@ export default class DataController { const batch = this.model.batch( 'transparent' ); batch.remove( ModelRange.createIn( modelRoot ) ); - batch.insert( this.parse( data ), modelRoot ); + batch.insert( this.parse( data, batch ), modelRoot ); } ); } @@ -208,16 +208,17 @@ export default class DataController { * * @see #set * @param {String} data Data to parse. + * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. * @param {String} [context='$root'] Base context in which the view will be converted to the model. See: * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}. * @returns {module:engine/model/documentfragment~DocumentFragment} Parsed data. */ - parse( data, context = '$root' ) { + parse( data, batch, context = '$root' ) { // data -> view const viewDocumentFragment = this.processor.toView( data ); // view -> model - return this.toModel( viewDocumentFragment, context ); + return this.toModel( viewDocumentFragment, batch, context ); } /** @@ -231,12 +232,13 @@ export default class DataController { * * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} viewElementOrFragment * Element or document fragment which content will be converted. + * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. * @param {String} [context='$root'] Base context in which the view will be converted to the model. See: * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher#convert}. * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment. */ - toModel( viewElementOrFragment, context = '$root' ) { - return this.viewToModel.convert( viewElementOrFragment, { context: [ context ] } ); + toModel( viewElementOrFragment, batch, context = '$root' ) { + return this.viewToModel.convert( viewElementOrFragment, { context: [ context ], batch } ); } /** diff --git a/src/controller/insertcontent.js b/src/controller/insertcontent.js index 9547592c8..4132d8875 100644 --- a/src/controller/insertcontent.js +++ b/src/controller/insertcontent.js @@ -229,7 +229,7 @@ class Insertion { // If the node is a text and bare text is allowed in current position it means that the node // contains disallowed attributes and we have to remove them. else if ( this.schema.check( { name: '$text', inside: this.position } ) ) { - this.schema.removeDisallowedAttributes( [ node ], this.position ); + this.schema.removeDisallowedAttributes( [ node ], this.position, this.batch ); this._handleNode( node, context ); } // If text is not allowed, try autoparagraphing. @@ -341,7 +341,7 @@ class Insertion { * @param {Object} context */ _tryAutoparagraphing( node, context ) { - const paragraph = new Element( 'paragraph' ); + const paragraph = this.batch.createElement( 'paragraph' ); // Do not autoparagraph if the paragraph won't be allowed there, // cause that would lead to an infinite loop. The paragraph would be rejected in @@ -350,7 +350,7 @@ class Insertion { // When node is a text and is disallowed by schema it means that contains disallowed attributes // and we need to remove them. if ( node.is( 'text' ) && !this._checkIsAllowed( node, [ paragraph ] ) ) { - this.schema.removeDisallowedAttributes( [ node ], [ paragraph ] ); + this.schema.removeDisallowedAttributes( [ node ], [ paragraph ], this.batch ); } if ( this._checkIsAllowed( node, [ paragraph ] ) ) { diff --git a/src/conversion/buildviewconverter.js b/src/conversion/buildviewconverter.js index ce7820046..bb4a120e9 100644 --- a/src/conversion/buildviewconverter.js +++ b/src/conversion/buildviewconverter.js @@ -8,9 +8,6 @@ */ import Matcher from '../view/matcher'; -import ModelElement from '../model/element'; -import ModelPosition from '../model/position'; -import modelWriter from '../model/writer'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; @@ -266,13 +263,15 @@ class ViewConverterBuilder { * buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' ); * buildViewConverter().for( dispatcher ) * .fromElement( 'img' ) - * .toElement( ( viewElement ) => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ); + * .toElement( ( viewElement, batch ) => batch.createElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) ); * * @param {String|Function} element Model element name or model element creator function. */ toElement( element ) { function eventCallbackGen( from ) { return ( evt, data, consumable, conversionApi ) => { + const batch = data.batch; + // There is one callback for all patterns in the matcher. // This will be usually just one pattern but we support matchers with many patterns too. const matchAll = from.matcher.matchAll( data.input ); @@ -285,7 +284,7 @@ class ViewConverterBuilder { // Now, for every match between matcher and actual element, we will try to consume the match. for ( const match of matchAll ) { // Create model element basing on creator function or element name. - const modelElement = element instanceof Function ? element( data.input ) : new ModelElement( element ); + const modelElement = element instanceof Function ? element( data.input, batch ) : batch.createElement( element ); // Do not convert if element building function returned falsy value. if ( !modelElement ) { @@ -310,8 +309,10 @@ class ViewConverterBuilder { // Convert children of converted view element and append them to `modelElement`. const modelChildren = conversionApi.convertChildren( data.input, consumable, data ); - const insertPosition = ModelPosition.createAt( modelElement, 'end' ); - modelWriter.insert( insertPosition, modelChildren ); + + for ( const child of Array.from( modelChildren ) ) { + batch.append( child, modelElement ); + } // Remove created `modelElement` from the parents stack. data.context.pop(); @@ -434,6 +435,8 @@ class ViewConverterBuilder { toMarker( creator ) { function eventCallbackGen( from ) { return ( evt, data, consumable ) => { + const batch = data.batch; + // There is one callback for all patterns in the matcher. // This will be usually just one pattern but we support matchers with many patterns too. const matchAll = from.matcher.matchAll( data.input ); @@ -450,7 +453,7 @@ class ViewConverterBuilder { modelElement = creator( data.input ); // When there is no creator then create model element basing on data from view element. } else { - modelElement = new ModelElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } ); + modelElement = batch.createElement( '$marker', { 'data-name': data.input.getAttribute( 'data-name' ) } ); } // Check if model element is correct (has proper name and property). @@ -525,7 +528,7 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) { }; if ( conversionApi.schema.check( schemaQuery ) ) { - toChange.setAttribute( attribute.key, attribute.value ); + data.batch.setAttribute( toChange, attribute.key, attribute.value ); } } diff --git a/src/conversion/view-to-model-converters.js b/src/conversion/view-to-model-converters.js index 24c872587..0652c82c2 100644 --- a/src/conversion/view-to-model-converters.js +++ b/src/conversion/view-to-model-converters.js @@ -3,10 +3,6 @@ * For licensing, see LICENSE.md. */ -import ModelDocumentFragment from '../model/documentfragment'; -import ModelText from '../model/text'; -import { normalizeNodes } from '../model/writer'; - /** * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for * {@link module:engine/conversion/viewconversiondispatcher~ViewConversionDispatcher}. @@ -33,9 +29,7 @@ export function convertToModelFragment() { return ( evt, data, consumable, conversionApi ) => { // Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement. if ( !data.output && consumable.consume( data.input, { name: true } ) ) { - const convertedChildren = conversionApi.convertChildren( data.input, consumable, data ); - - data.output = new ModelDocumentFragment( normalizeNodes( convertedChildren ) ); + data.output = conversionApi.convertChildren( data.input, consumable, data ); } }; } @@ -54,7 +48,7 @@ export function convertText() { if ( conversionApi.schema.check( schemaQuery ) ) { if ( consumable.consume( data.input ) ) { - data.output = new ModelText( data.input.data ); + data.output = data.batch.createText( data.input.data ); } } }; diff --git a/src/conversion/viewconversiondispatcher.js b/src/conversion/viewconversiondispatcher.js index 4a9100cac..32da4824f 100644 --- a/src/conversion/viewconversiondispatcher.js +++ b/src/conversion/viewconversiondispatcher.js @@ -13,7 +13,6 @@ import ModelPosition from '../model/position'; import ModelTreeWalker from '../model/treewalker'; import ModelNode from '../model/node'; import ModelDocumentFragment from '../model/documentfragment'; -import { remove } from '../model/writer'; import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; @@ -134,13 +133,16 @@ export default class ViewConversionDispatcher { * @fires documentFragment * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem * Part of the view to be converted. - * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher` + * @param {Object} additionalData Additional data to be passed in `data` argument when firing `ViewConversionDispatcher` * events. See also {@link ~ViewConversionDispatcher#event:element element event}. + * @param {module:engine/model/batch~Batch} additionalData.batch Batch to which the deltas will be added. * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}. */ - convert( viewItem, additionalData = {} ) { + convert( viewItem, additionalData ) { + const batch = additionalData.batch; + this.fire( 'viewCleanup', viewItem ); const consumable = ViewConsumable.createFrom( viewItem ); @@ -149,16 +151,19 @@ export default class ViewConversionDispatcher { // We can get a null here if conversion failed (see _convertItem()) // or simply if an item could not be converted (e.g. due to the schema). if ( !conversionResult ) { - return new ModelDocumentFragment(); + return batch.createDocumentFragment(); } // When conversion result is not a document fragment we need to wrap it in document fragment. if ( !conversionResult.is( 'documentFragment' ) ) { - conversionResult = new ModelDocumentFragment( [ conversionResult ] ); + const docFrag = batch.createDocumentFragment(); + + batch.append( conversionResult, docFrag ); + conversionResult = docFrag; } // Extract temporary markers elements from model and set as static markers collection. - conversionResult.markers = extractMarkersFromModelFragment( conversionResult ); + conversionResult.markers = extractMarkersFromModelFragment( conversionResult, batch ); return conversionResult; } @@ -203,24 +208,23 @@ export default class ViewConversionDispatcher { * @private * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren */ - _convertChildren( input, consumable, additionalData = {} ) { - // Get all children of view input item. - const viewChildren = Array.from( input.getChildren() ); - - // 1. Map those children to model. - // 2. Filter out items that has not been converted or for which conversion returned wrong result (for those warning is logged). - // 3. Extract children from document fragments to flatten results. - const convertedChildren = viewChildren - .map( viewChild => this._convertItem( viewChild, consumable, additionalData ) ) - .filter( converted => converted instanceof ModelNode || converted instanceof ModelDocumentFragment ) - .reduce( ( result, filtered ) => { - return result.concat( - filtered.is( 'documentFragment' ) ? Array.from( filtered.getChildren() ) : filtered - ); - }, [] ); - - // Normalize array to model document fragment. - return new ModelDocumentFragment( convertedChildren ); + _convertChildren( input, consumable, additionalData ) { + const batch = additionalData.batch; + const documentFragment = batch.createDocumentFragment(); + + for ( const viewChild of Array.from( input.getChildren() ) ) { + const modelChild = this._convertItem( viewChild, consumable, additionalData ); + + if ( modelChild instanceof ModelNode ) { + batch.append( modelChild, documentFragment ); + } else if ( modelChild instanceof ModelDocumentFragment ) { + for ( const child of Array.from( modelChild ) ) { + batch.append( child, documentFragment ); + } + } + } + + return documentFragment; } /** @@ -278,7 +282,7 @@ mix( ViewConversionDispatcher, EmitterMixin ); // // @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model. // @returns {Map} List of static markers. -function extractMarkersFromModelFragment( modelItem ) { +function extractMarkersFromModelFragment( modelItem, batch ) { const markerElements = new Set(); const markers = new Map(); @@ -310,7 +314,7 @@ function extractMarkersFromModelFragment( modelItem ) { } // Remove marker element from DocumentFragment. - remove( ModelRange.createOn( markerElement ) ); + batch.remove( markerElement ); } return markers; diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 9fa8650e9..2c8103f48 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -96,8 +96,10 @@ export function setData( document, data, options = {} ) { let modelDocumentFragment, selection; const modelRoot = document.getRoot( options.rootName || 'main' ); + const batch = document.batch( options.batchType || 'transparent' ); + // Parse data string to model. - const parsedResult = setData._parse( data, document.schema, { + const parsedResult = setData._parse( data, document.schema, batch, { lastRangeBackward: options.lastRangeBackward, selectionAttributes: options.selectionAttributes, context: [ modelRoot.name ] @@ -113,8 +115,6 @@ export function setData( document, data, options = {} ) { document.enqueueChanges( () => { // Replace existing model in document by new one. - const batch = document.batch( options.batchType || 'transparent' ); - batch.remove( ModelRange.createIn( modelRoot ) ); batch.insert( modelDocumentFragment, modelRoot ); @@ -243,7 +243,8 @@ export function stringify( node, selectionOrPositionOrRange = null ) { * * @param {String} data HTML-like string to be parsed. * @param {module:engine/model/schema~Schema} schema Schema instance uses by converters for element validation. - * @param {Object} options Additional configuration. + * @param {module:engine/model/batch~Batch} batch Batch used for conversion. + * @param {Object} [options={}] Additional configuration. * @param {Array} [options.selectionAttributes] List of attributes which will be passed to the selection. * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward. * @param {module:engine/model/schema~SchemaPath} [options.context=[ '$root' ]] The conversion context. @@ -252,7 +253,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) { * module:engine/model/documentfragment~DocumentFragment|Object} Returns parsed model node or * object with two fields `model` and `selection` when selection ranges were included in data to parse. */ -export function parse( data, schema, options = {} ) { +export function parse( data, schema, batch, options = {} ) { const mapper = new Mapper(); // Replace not accepted by XML `$text` tag name by valid one `model-text-with-attributes`. @@ -283,7 +284,7 @@ export function parse( data, schema, options = {} ) { viewToModel.on( 'text', convertToModelText() ); // Convert view to model. - let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } ); + let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ], batch } ); // If root DocumentFragment contains only one element - return that element. if ( model.is( 'documentFragment' ) && model.childCount == 1 ) { diff --git a/src/model/document.js b/src/model/document.js index 018d1e1c7..4d023e4d9 100644 --- a/src/model/document.js +++ b/src/model/document.js @@ -168,9 +168,8 @@ export default class Document { if ( operation.isDocumentOperation ) { this.version++; this.history.addDelta( operation.delta ); + this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type ); } - - this.fire( 'change', operation.type, changes, operation.delta.batch, operation.delta.type ); } /** diff --git a/src/model/schema.js b/src/model/schema.js index 6a5a0b0e8..1b454059a 100644 --- a/src/model/schema.js +++ b/src/model/schema.js @@ -414,14 +414,11 @@ export default class Schema { } /** - * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes. - * When {@link module:engine/model/batch~Batch batch} parameter is provided then attributes will be removed - * using that batch, by creating {@link module:engine/model/delta/attributedelta~AttributeDelta attribute deltas}. - * Otherwise, attributes will be removed directly from provided nodes using {@link module:engine/model/node~Node node} API. + * Removes disallowed by {@link module:engine/model/schema~Schema schema} attributes from given nodes.. * * @param {Iterable.} nodes Nodes that will be filtered. * @param {module:engine/model/schema~SchemaPath} inside Path inside which schema will be checked. - * @param {module:engine/model/batch~Batch} [batch] Batch to which the deltas will be added. + * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. */ removeDisallowedAttributes( nodes, inside, batch ) { for ( const node of nodes ) { @@ -435,11 +432,7 @@ export default class Schema { // TODO: this should be improved to check all combination of attributes. for ( const attribute of node.getAttributeKeys() ) { if ( !this.check( { name, attributes: attribute, inside: queryPath } ) ) { - if ( batch ) { - batch.removeAttribute( node, attribute ); - } else { - node.removeAttribute( attribute ); - } + batch.removeAttribute( node, attribute ); } } } diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js index 53e403b65..f20861294 100644 --- a/tests/controller/datacontroller.js +++ b/tests/controller/datacontroller.js @@ -48,7 +48,7 @@ describe( 'DataController', () => { describe( 'parse', () => { it( 'should set text', () => { schema.allow( { name: '$text', inside: '$root' } ); - const model = data.parse( '

foobar

' ); + const model = data.parse( '

foobar

', modelDocument.batch() ); expect( model ).to.instanceof( ModelDocumentFragment ); expect( stringify( model ) ).to.equal( 'foobar' ); @@ -59,7 +59,7 @@ describe( 'DataController', () => { buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' ); - const model = data.parse( '

foobar

' ); + const model = data.parse( '

foobar

', modelDocument.batch() ); expect( model ).to.instanceof( ModelDocumentFragment ); expect( stringify( model ) ).to.equal( 'foobar' ); @@ -70,7 +70,7 @@ describe( 'DataController', () => { buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' ); - const model = data.parse( '

foo

bar

' ); + const model = data.parse( '

foo

bar

', modelDocument.batch() ); expect( model ).to.instanceof( ModelDocumentFragment ); expect( stringify( model ) ).to.equal( 'foobar' ); @@ -83,20 +83,20 @@ describe( 'DataController', () => { buildViewConverter().for( data.viewToModel ).fromElement( 'p' ).toElement( 'paragraph' ); buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', true ); - const model = data.parse( '

foobar

' ); + const model = data.parse( '

foobar

', modelDocument.batch() ); expect( model ).to.instanceof( ModelDocumentFragment ); expect( stringify( model ) ).to.equal( 'foo<$text bold="true">bar' ); } ); it( 'should parse in the root context by default', () => { - const model = data.parse( 'foo' ); + const model = data.parse( 'foo', modelDocument.batch() ); expect( stringify( model ) ).to.equal( '' ); } ); it( 'should accept parsing context', () => { - const model = data.parse( 'foo', '$block' ); + const model = data.parse( 'foo', modelDocument.batch(), '$block' ); expect( stringify( model ) ).to.equal( 'foo' ); } ); @@ -111,7 +111,7 @@ describe( 'DataController', () => { it( 'should convert content of an element #1', () => { const viewElement = parseView( '

foo

' ); - const model = data.toModel( viewElement ); + const model = data.toModel( viewElement, modelDocument.batch() ); expect( model ).to.instanceof( ModelDocumentFragment ); expect( stringify( model ) ).to.equal( 'foo' ); @@ -119,7 +119,7 @@ describe( 'DataController', () => { it( 'should convert content of an element #2', () => { const viewFragment = parseView( '

foo

bar

' ); - const model = data.toModel( viewFragment ); + const model = data.toModel( viewFragment, modelDocument.batch() ); expect( model ).to.be.instanceOf( ModelDocumentFragment ); expect( stringify( model ) ).to.equal( 'foobar' ); @@ -134,10 +134,10 @@ describe( 'DataController', () => { const viewFragment = new ViewDocumentFragment( [ parseView( 'foo' ) ] ); // Model fragment in root. - expect( stringify( data.toModel( viewFragment ) ) ).to.equal( '' ); + expect( stringify( data.toModel( viewFragment, modelDocument.batch() ) ) ).to.equal( '' ); // Model fragment in inline root. - expect( stringify( data.toModel( viewFragment, 'inlineRoot' ) ) ).to.equal( 'foo' ); + expect( stringify( data.toModel( viewFragment, modelDocument.batch(), 'inlineRoot' ) ) ).to.equal( 'foo' ); } ); } ); @@ -264,6 +264,8 @@ describe( 'DataController', () => { } ); describe( 'stringify', () => { + let batch; + beforeEach( () => { modelDocument.schema.registerItem( 'paragraph', '$block' ); modelDocument.schema.registerItem( 'div' ); @@ -272,16 +274,18 @@ describe( 'DataController', () => { modelDocument.schema.allow( { name: 'div', inside: '$root' } ); buildModelConverter().for( data.modelToView ).fromElement( 'paragraph' ).toElement( 'p' ); + + batch = modelDocument.batch(); } ); it( 'should stringify a content of an element', () => { - const modelElement = parseModel( '
foo
', modelDocument.schema ); + const modelElement = parseModel( '
foo
', modelDocument.schema, batch ); expect( data.stringify( modelElement ) ).to.equal( '

foo

' ); } ); it( 'should stringify a content of a document fragment', () => { - const modelDocumentFragment = parseModel( 'foobar', modelDocument.schema ); + const modelDocumentFragment = parseModel( 'foobar', modelDocument.schema, batch ); expect( data.stringify( modelDocumentFragment ) ).to.equal( '

foo

bar

' ); } ); @@ -299,7 +303,7 @@ describe( 'DataController', () => { } ); it( 'should convert a content of an element', () => { - const modelElement = parseModel( '
foo
', modelDocument.schema ); + const modelElement = parseModel( '
foo
', modelDocument.schema, modelDocument.batch() ); const viewDocumentFragment = data.toView( modelElement ); @@ -313,7 +317,11 @@ describe( 'DataController', () => { } ); it( 'should convert a document fragment', () => { - const modelDocumentFragment = parseModel( 'foobar', modelDocument.schema ); + const modelDocumentFragment = parseModel( + 'foobar', + modelDocument.schema, + modelDocument.batch() + ); const viewDocumentFragment = data.toView( modelDocumentFragment ); diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js index 43180b503..0cd160e5e 100644 --- a/tests/controller/editingcontroller.js +++ b/tests/controller/editingcontroller.js @@ -155,7 +155,8 @@ describe( 'EditingController', () => { 'foo' + '' + 'bar', - model.schema + model.schema, + model.batch() )._children ); model.enqueueChanges( () => { @@ -409,9 +410,11 @@ describe( 'EditingController', () => { editing.destroy(); + const batch = model.batch(); + model.enqueueChanges( () => { - const modelData = parse( 'foo', model.schema ).getChild( 0 ); - model.batch().insert( modelData, model.getRoot() ); + const modelData = parse( 'foo', model.schema, batch ).getChild( 0 ); + batch.insert( modelData, model.getRoot() ); } ); expect( spy.called ).to.be.false; diff --git a/tests/controller/insertcontent.js b/tests/controller/insertcontent.js index 64d0373af..4a11b6ac2 100644 --- a/tests/controller/insertcontent.js +++ b/tests/controller/insertcontent.js @@ -756,12 +756,14 @@ describe( 'DataController', () => { // // @param {module:engine/model/item~Item|String} content function insertHelper( content ) { + const batch = doc.batch(); + if ( typeof content == 'string' ) { - content = parse( content, doc.schema, { + content = parse( content, doc.schema, batch, { context: [ '$clipboardHolder' ] } ); } - insertContent( dataController, content, doc.selection ); + insertContent( dataController, content, doc.selection, batch ); } } ); diff --git a/tests/conversion/advanced-converters.js b/tests/conversion/advanced-converters.js index 02b7bbfee..9cccf90e7 100644 --- a/tests/conversion/advanced-converters.js +++ b/tests/conversion/advanced-converters.js @@ -40,12 +40,13 @@ import { convertToModelFragment, convertText } from '../../src/conversion/view-t import { createRangeOnElementOnly } from '../../tests/model/_utils/utils'; describe( 'advanced-converters', () => { - let modelDoc, modelRoot, viewRoot, mapper, modelDispatcher, viewDispatcher; + let modelDoc, modelRoot, viewRoot, mapper, modelDispatcher, viewDispatcher, batch; beforeEach( () => { modelDoc = new ModelDocument(); modelRoot = modelDoc.createRoot(); viewRoot = new ViewContainerElement( 'div' ); + batch = modelDoc.batch(); mapper = new Mapper(); mapper.bindElements( modelRoot, viewRoot ); @@ -207,8 +208,8 @@ describe( 'advanced-converters', () => { const viewFigureConverter = function( evt, data, consumable, conversionApi ) { if ( consumable.consume( data.input, { name: true } ) ) { - const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable ); - const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable ); + const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable, { batch } ); + const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable, { batch } ); modelImage.appendChildren( modelCaption ); @@ -231,7 +232,7 @@ describe( 'advanced-converters', () => { const viewFigcaptionConverter = function( evt, data, consumable, conversionApi ) { if ( consumable.consume( data.input, { name: true } ) ) { const modelCaption = new ModelElement( 'caption' ); - const children = conversionApi.convertChildren( data.input, consumable ); + const children = conversionApi.convertChildren( data.input, consumable, { batch } ); modelCaption.appendChildren( children ); @@ -286,7 +287,7 @@ describe( 'advanced-converters', () => { it( 'should convert view image to model', () => { const viewElement = new ViewContainerElement( 'img', { src: 'bar.jpg', title: 'bar' } ); - const modelElement = viewDispatcher.convert( viewElement ); + const modelElement = viewDispatcher.convert( viewElement, { batch } ); expect( modelToString( modelElement ) ).to.equal( '' ); } ); @@ -300,7 +301,7 @@ describe( 'advanced-converters', () => { new ViewContainerElement( 'figcaption', null, new ViewText( 'foobar' ) ) ] ); - const modelElement = viewDispatcher.convert( viewElement ); + const modelElement = viewDispatcher.convert( viewElement, { batch } ); expect( modelToString( modelElement ) ).to.equal( 'foobar' ); } ); @@ -371,7 +372,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable ); + data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); } for ( const child of data.output ) { @@ -383,7 +384,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { attribute: 'title' } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable ); + data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); } for ( const child of data.output ) { @@ -468,7 +469,7 @@ describe( 'advanced-converters', () => { } } - const children = conversionApi.convertChildren( data.input, consumable ); + const children = conversionApi.convertChildren( data.input, consumable, { batch } ); data.output.appendChildren( children ); } } ); @@ -519,7 +520,7 @@ describe( 'advanced-converters', () => { it( 'should convert a view element to model', () => { const viewElement = new ViewAttributeElement( 'a', { href: 'foo.html', title: 'Foo title' }, new ViewText( 'foo' ) ); - const modelText = viewDispatcher.convert( viewElement ).getChild( 0 ); + const modelText = viewDispatcher.convert( viewElement, { batch } ).getChild( 0 ); expect( modelText ).to.be.instanceof( ModelText ); expect( modelText.data ).to.equal( 'foo' ); @@ -590,7 +591,7 @@ describe( 'advanced-converters', () => { ] ); - const modelElement = viewDispatcher.convert( viewElement ); + const modelElement = viewDispatcher.convert( viewElement, { batch } ); expect( modelToString( modelElement ) ).to.equal( 'foo' ); } ); @@ -602,7 +603,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable ); + data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); } for ( const child of data.output ) { @@ -615,7 +616,7 @@ describe( 'advanced-converters', () => { if ( consumable.consume( data.input, { name: true } ) ) { data.output = new ModelElement( 'paragraph' ); - const children = conversionApi.convertChildren( data.input, consumable ); + const children = conversionApi.convertChildren( data.input, consumable, { batch } ); for ( let i = 1; i < children.childCount; i++ ) { const child = children.getChild( i ); @@ -632,13 +633,13 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:table', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true } ) ) { - data.output = conversionApi.convertChildren( data.input, consumable ); + data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); } } ); viewDispatcher.on( 'element:td', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true } ) ) { - data.output = conversionApi.convertChildren( data.input, consumable ); + data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); } } ); @@ -653,7 +654,7 @@ describe( 'advanced-converters', () => { ] ) ] ); - const model = viewDispatcher.convert( viewTable ); + const model = viewDispatcher.convert( viewTable, { batch } ); const modelFragment = new ModelDocumentFragment( model ); expect( modelToString( modelFragment ) ) @@ -680,7 +681,7 @@ describe( 'advanced-converters', () => { } } - data.output.appendChildren( conversionApi.convertChildren( data.input, consumable ) ); + data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, { batch } ) ); } }, { priority: 'lowest' } ); @@ -704,7 +705,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:strong', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable ); + data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); } for ( const child of data.output ) { @@ -755,7 +756,7 @@ describe( 'advanced-converters', () => { ] ) ] ); - const modelElement = viewDispatcher.convert( viewElement ); + const modelElement = viewDispatcher.convert( viewElement, { batch } ); expect( modelToString( modelElement ) ).to.equal( '' + diff --git a/tests/conversion/buildviewconverter.js b/tests/conversion/buildviewconverter.js index 0b485f10b..9bf2a6065 100644 --- a/tests/conversion/buildviewconverter.js +++ b/tests/conversion/buildviewconverter.js @@ -6,6 +6,7 @@ import buildViewConverter from '../../src/conversion/buildviewconverter'; import ModelSchema from '../../src/model/schema'; +import ModelDocument from '../../src/model/document'; import ModelDocumentFragment from '../../src/model/documentfragment'; import ModelElement from '../../src/model/element'; import ModelTextProxy from '../../src/model/textproxy'; @@ -63,11 +64,15 @@ const textAttributes = [ undefined, 'linkHref', 'linkTitle', 'bold', 'italic', ' const pAttributes = [ undefined, 'class', 'important', 'theme', 'decorated', 'size' ]; describe( 'View converter builder', () => { - let dispatcher, schema, objWithContext; + let dispatcher, schema, additionalData, batch; + + const modelDocument = new ModelDocument(); beforeEach( () => { + batch = modelDocument.batch(); + // `additionalData` parameter for `.convert` calls. - objWithContext = { context: [ '$root' ] }; + additionalData = { context: [ '$root' ], batch }; schema = new ModelSchema(); @@ -95,7 +100,7 @@ describe( 'View converter builder', () => { it( 'should convert from view element to model element', () => { buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' ); - const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), objWithContext ); + const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -105,7 +110,7 @@ describe( 'View converter builder', () => { .fromElement( 'img' ) .toElement( viewElement => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) ); - const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), objWithContext ); + const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); } ); @@ -114,7 +119,7 @@ describe( 'View converter builder', () => { buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true ); const conversionResult = dispatcher.convert( - new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), objWithContext + new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData ); // Have to check root because result is a ModelText. @@ -127,7 +132,7 @@ describe( 'View converter builder', () => { .toAttribute( viewElement => ( { key: 'linkHref', value: viewElement.getAttribute( 'href' ) } ) ); const conversionResult = dispatcher.convert( - new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), objWithContext + new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), additionalData ); // Have to check root because result is a ModelText. @@ -142,7 +147,7 @@ describe( 'View converter builder', () => { .toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) ); const conversionResult = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); @@ -164,7 +169,7 @@ describe( 'View converter builder', () => { new ViewContainerElement( 'p', { 'data-type': 'foo' }, new ViewText( 'xyz' ) ) ] ); - const conversionResult = dispatcher.convert( viewStructure, objWithContext ); + const conversionResult = dispatcher.convert( viewStructure, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' + @@ -190,7 +195,7 @@ describe( 'View converter builder', () => { new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) ) ] ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '<$text bold="true">aaabbbcccddd' ); } ); @@ -207,7 +212,7 @@ describe( 'View converter builder', () => { new ViewText( 'r' ) ] ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); const markerSearch = conversionResult.markers.get( 'search' ); @@ -229,7 +234,7 @@ describe( 'View converter builder', () => { new ViewText( 'r' ) ] ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); const markerSearch = conversionResult.markers.get( 'search' ); @@ -255,7 +260,7 @@ describe( 'View converter builder', () => { new ViewText( 'r' ) ] ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); const marker1 = conversionResult.markers.get( 'marker1' ); const marker2 = conversionResult.markers.get( 'marker2' ); @@ -272,7 +277,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'span' ); - const result = dispatcher.convert( element, objWithContext ); + const result = dispatcher.convert( element, additionalData ); expect( result ).to.be.instanceof( ModelDocumentFragment ); expect( result.childCount ).to.equal( 0 ); @@ -284,7 +289,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'marker', { class: 'search' } ); expect( () => { - dispatcher.convert( element, objWithContext ); + dispatcher.convert( element, additionalData ); } ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ ); } ); @@ -296,7 +301,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } ); expect( () => { - dispatcher.convert( element, objWithContext ); + dispatcher.convert( element, additionalData ); } ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ ); } ); @@ -308,7 +313,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } ); expect( () => { - dispatcher.convert( element, objWithContext ); + dispatcher.convert( element, additionalData ); } ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ ); } ); @@ -325,7 +330,7 @@ describe( 'View converter builder', () => { // Not quite megatron. result = dispatcher.convert( - new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), objWithContext + new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -333,7 +338,7 @@ describe( 'View converter builder', () => { // Almost a megatron. Missing a head. result = dispatcher.convert( new ViewContainerElement( 'span', { class: 'megatron', body: 'megatron', legs: 'megatron' }, new ViewText( 'foo' ) ), - objWithContext + additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -345,7 +350,7 @@ describe( 'View converter builder', () => { { class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' }, new ViewText( 'foo' ) ), - objWithContext + additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -357,7 +362,7 @@ describe( 'View converter builder', () => { { class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' }, new ViewText( 'foo' ) ), - objWithContext + additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -377,7 +382,7 @@ describe( 'View converter builder', () => { new ViewText( 'foo' ) ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -387,7 +392,7 @@ describe( 'View converter builder', () => { const viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); expect( conversionResult.is( 'documentFragment' ) ).to.be.true; } ); @@ -399,7 +404,7 @@ describe( 'View converter builder', () => { buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' ); const conversionResult = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData ); // Element converter was fired first even though attribute converter was added first. @@ -415,7 +420,7 @@ describe( 'View converter builder', () => { let result; result = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -425,7 +430,7 @@ describe( 'View converter builder', () => { .toElement( 'customP' ); result = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), objWithContext + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -446,7 +451,7 @@ describe( 'View converter builder', () => { .toAttribute( 'size', 'small' ); const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); // P element and it's children got converted by the converter (1) and the converter (1) got fired // because P name was not consumed in converter (2). Converter (3) could consume class="small" because @@ -469,7 +474,7 @@ describe( 'View converter builder', () => { new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) ) ] ); - const conversionResult = dispatcher.convert( viewStructure, objWithContext ); + const conversionResult = dispatcher.convert( viewStructure, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '
foo
' ); } ); @@ -488,7 +493,7 @@ describe( 'View converter builder', () => { ) ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -507,7 +512,7 @@ describe( 'View converter builder', () => { ) ); - const conversionResult = dispatcher.convert( viewElement, objWithContext ); + const conversionResult = dispatcher.convert( viewElement, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -521,11 +526,11 @@ describe( 'View converter builder', () => { } ); const viewElement = new ViewContainerElement( 'p' ); - let conversionResult = dispatcher.convert( viewElement, objWithContext ); + let conversionResult = dispatcher.convert( viewElement, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); viewElement.setAttribute( 'stop', true ); - conversionResult = dispatcher.convert( viewElement, objWithContext ); + conversionResult = dispatcher.convert( viewElement, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 0 ); @@ -543,11 +548,11 @@ describe( 'View converter builder', () => { } ); const viewElement = new ViewContainerElement( 'p', { 'data-type': 'foo' } ); - let conversionResult = dispatcher.convert( viewElement, objWithContext ); + let conversionResult = dispatcher.convert( viewElement, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); viewElement.setAttribute( 'data-type', 'stop' ); - conversionResult = dispatcher.convert( viewElement, objWithContext ); + conversionResult = dispatcher.convert( viewElement, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); } ); } ); diff --git a/tests/conversion/view-to-model-converters.js b/tests/conversion/view-to-model-converters.js index fae127644..b4f19525c 100644 --- a/tests/conversion/view-to-model-converters.js +++ b/tests/conversion/view-to-model-converters.js @@ -9,6 +9,7 @@ import ViewDocumentFragment from '../../src/view/documentfragment'; import ViewText from '../../src/view/text'; import ModelSchema from '../../src/model/schema'; +import ModelDocument from '../../src/model/document'; import ModelDocumentFragment from '../../src/model/documentfragment'; import ModelElement from '../../src/model/element'; import ModelText from '../../src/model/text'; @@ -16,13 +17,16 @@ import ModelText from '../../src/model/text'; import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters'; describe( 'view-to-model-converters', () => { - let dispatcher, schema, objWithContext; + let dispatcher, schema, additionalData, batch; + + const modelDocument = new ModelDocument(); beforeEach( () => { schema = new ModelSchema(); schema.registerItem( 'paragraph', '$block' ); schema.allow( { name: '$text', inside: '$root' } ); - objWithContext = { context: [ '$root' ] }; + batch = modelDocument.batch(); + additionalData = { context: [ '$root' ], batch }; dispatcher = new ViewConversionDispatcher( { schema } ); } ); @@ -32,7 +36,7 @@ describe( 'view-to-model-converters', () => { dispatcher.on( 'text', convertText() ); - const conversionResult = dispatcher.convert( viewText, objWithContext ); + const conversionResult = dispatcher.convert( viewText, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -51,7 +55,7 @@ describe( 'view-to-model-converters', () => { } } ); - const conversionResult = dispatcher.convert( viewText, objWithContext ); + const conversionResult = dispatcher.convert( viewText, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -64,12 +68,12 @@ describe( 'view-to-model-converters', () => { const viewText = new ViewText( 'foobar' ); dispatcher.on( 'text', convertText() ); - let conversionResult = dispatcher.convert( viewText, objWithContext ); + let conversionResult = dispatcher.convert( viewText, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 0 ); - conversionResult = dispatcher.convert( viewText, { context: [ '$block' ] } ); + conversionResult = dispatcher.convert( viewText, { context: [ '$block' ], batch } ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 1 ); @@ -82,7 +86,7 @@ describe( 'view-to-model-converters', () => { dispatcher.on( 'text', convertText() ); - const conversionResult = dispatcher.convert( viewText, objWithContext ); + const conversionResult = dispatcher.convert( viewText, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -103,7 +107,7 @@ describe( 'view-to-model-converters', () => { dispatcher.on( 'element', convertToModelFragment() ); dispatcher.on( 'documentFragment', convertToModelFragment() ); - const conversionResult = dispatcher.convert( viewFragment, objWithContext ); + const conversionResult = dispatcher.convert( viewFragment, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.maxOffset ).to.equal( 6 ); @@ -128,7 +132,7 @@ describe( 'view-to-model-converters', () => { } } ); - const conversionResult = dispatcher.convert( viewP, objWithContext ); + const conversionResult = dispatcher.convert( viewP, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement ); diff --git a/tests/conversion/viewconversiondispatcher.js b/tests/conversion/viewconversiondispatcher.js index a3a0dd450..1ef8728fc 100644 --- a/tests/conversion/viewconversiondispatcher.js +++ b/tests/conversion/viewconversiondispatcher.js @@ -11,6 +11,7 @@ import ViewText from '../../src/view/text'; import ModelText from '../../src/model/text'; import ModelElement from '../../src/model/element'; import ModelDocumentFragment from '../../src/model/documentfragment'; +import ModelDocument from '../../src/model/document'; import { stringify } from '../../src/dev-utils/model'; import log from '@ckeditor/ckeditor5-utils/src/log'; @@ -35,10 +36,13 @@ describe( 'ViewConversionDispatcher', () => { } ); describe( 'convert', () => { - let dispatcher; + let dispatcher, batch; + + const modelDocument = new ModelDocument(); beforeEach( () => { dispatcher = new ViewConversionDispatcher(); + batch = modelDocument.batch(); } ); it( 'should fire viewCleanup event on converted view part', () => { @@ -47,7 +51,7 @@ describe( 'ViewConversionDispatcher', () => { sinon.spy( dispatcher, 'fire' ); const viewP = new ViewContainerElement( 'p' ); - dispatcher.convert( viewP ); + dispatcher.convert( viewP, { batch } ); expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true; } ); @@ -61,9 +65,9 @@ describe( 'ViewConversionDispatcher', () => { sinon.spy( dispatcher, 'fire' ); - dispatcher.convert( viewText ); - dispatcher.convert( viewElement ); - dispatcher.convert( viewFragment ); + dispatcher.convert( viewText, { batch } ); + dispatcher.convert( viewElement, { batch } ); + dispatcher.convert( viewFragment, { batch } ); expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true; expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true; @@ -95,7 +99,7 @@ describe( 'ViewConversionDispatcher', () => { } ); // Use `additionalData` parameter to check if it was passed to the event. - const conversionResult = dispatcher.convert( viewText, { foo: 'bar' } ); + const conversionResult = dispatcher.convert( viewText, { foo: 'bar', batch } ); // Check conversion result. // Result should be wrapped in document fragment. @@ -130,7 +134,7 @@ describe( 'ViewConversionDispatcher', () => { } ); // Use `additionalData` parameter to check if it was passed to the event. - const conversionResult = dispatcher.convert( viewElement, { foo: 'bar' } ); + const conversionResult = dispatcher.convert( viewElement, { foo: 'bar', batch } ); // Check conversion result. // Result should be wrapped in document fragment. @@ -164,7 +168,7 @@ describe( 'ViewConversionDispatcher', () => { } ); // Use `additionalData` parameter to check if it was passed to the event. - const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar' } ); + const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar', batch } ); // Check conversion result. expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); @@ -187,7 +191,7 @@ describe( 'ViewConversionDispatcher', () => { ] ); } ); - const conversionResult = dispatcher.convert( viewFragment ); + const conversionResult = dispatcher.convert( viewFragment, { batch } ); expect( conversionResult.markers.size ).to.equal( 2 ); expect( stringify( conversionResult, conversionResult.markers.get( 'marker1' ) ) ).to.deep.equal( 'fo[ob]ar' ); @@ -197,9 +201,13 @@ describe( 'ViewConversionDispatcher', () => { describe( 'conversionApi', () => { let spy, spyP, spyText, viewP, viewText, modelP, modelText, consumableMock, dispatcher, - spyNull, spyArray, viewDiv, viewNull, viewArray; + spyNull, spyArray, viewDiv, viewNull, viewArray, batch; + + const modelDocument = new ModelDocument(); beforeEach( () => { + batch = modelDocument.batch(); + spy = sinon.spy(); spyP = sinon.spy(); spyText = sinon.spy(); @@ -260,9 +268,10 @@ describe( 'ViewConversionDispatcher', () => { expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP ); expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText ); + expect( data.batch ).to.equal( batch ); } ); - dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar' } ); + dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar', batch } ); expect( spy.calledOnce ).to.be.true; expect( spyP.calledOnce ).to.be.true; @@ -279,7 +288,7 @@ describe( 'ViewConversionDispatcher', () => { expect( conversionApi.convertItem( viewNull ) ).to.equal( null ); } ); - dispatcher.convert( new ViewDocumentFragment() ); + dispatcher.convert( new ViewDocumentFragment(), { batch } ); expect( spy.calledOnce ).to.be.true; expect( spyNull.calledOnce ).to.be.true; @@ -297,7 +306,7 @@ describe( 'ViewConversionDispatcher', () => { expect( conversionApi.convertItem( viewArray ) ).to.equal( null ); } ); - dispatcher.convert( new ViewDocumentFragment() ); + dispatcher.convert( new ViewDocumentFragment(), { batch } ); expect( spy.calledOnce ).to.be.true; expect( spyArray.calledOnce ).to.be.true; @@ -323,7 +332,7 @@ describe( 'ViewConversionDispatcher', () => { expect( result.getChild( 1 ) ).to.equal( modelText ); } ); - dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar' } ); + dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar', batch } ); expect( spy.calledOnce ).to.be.true; expect( spyP.calledOnce ).to.be.true; @@ -344,7 +353,7 @@ describe( 'ViewConversionDispatcher', () => { expect( result.getChild( 1 ) ).to.equal( modelText ); } ); - dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar' } ); + dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar', batch } ); expect( spy.calledOnce ).to.be.true; expect( spyNull.calledOnce ).to.be.true; diff --git a/tests/dev-utils/model.js b/tests/dev-utils/model.js index 322019965..c38c2d4d2 100644 --- a/tests/dev-utils/model.js +++ b/tests/dev-utils/model.js @@ -497,21 +497,21 @@ describe( 'model test utils', () => { it( 'throws when invalid XML', () => { expect( () => { - parse( '', document.schema ); + parse( '', document.schema, document.batch() ); } ).to.throw( Error, /Parse error/ ); } ); it( 'throws when try to set element not registered in schema', () => { expect( () => { - parse( '', document.schema ); + parse( '', document.schema, document.batch() ); } ).to.throw( Error, 'Element \'xyz\' not allowed in context ["$root"].' ); } ); it( 'throws when try to set text directly to $root without registering it', () => { - const doc = new Document(); + const document = new Document(); expect( () => { - parse( 'text', doc.schema ); + parse( 'text', document.schema, document.batch() ); } ).to.throw( Error, 'Element \'$text\' not allowed in context ["$root"].' ); } ); @@ -521,7 +521,7 @@ describe( 'model test utils', () => { doc.schema.allow( { name: '$text', inside: 'foo' } ); expect( () => { - parse( 'text', doc.schema, { context: [ 'foo' ] } ); + parse( 'text', doc.schema, doc.batch(), { context: [ 'foo' ] } ); } ).to.not.throw(); } ); @@ -556,7 +556,7 @@ describe( 'model test utils', () => { } ); it( 'sets selection attributes', () => { - const result = parse( 'foo[]bar', document.schema, { selectionAttributes: { + const result = parse( 'foo[]bar', document.schema, document.batch(), { selectionAttributes: { bold: true, italic: true } } ); @@ -577,7 +577,7 @@ describe( 'model test utils', () => { } ); it( 'sets selection with attribute containing an element', () => { - const result = parse( 'x[]', document.schema, { selectionAttributes: { + const result = parse( 'x[]', document.schema, document.batch(), { selectionAttributes: { bold: true } } ); @@ -586,7 +586,7 @@ describe( 'model test utils', () => { } ); it( 'sets a backward selection containing an element', () => { - const result = parse( 'x[]', document.schema, { + const result = parse( 'x[]', document.schema, document.batch(), { lastRangeBackward: true } ); @@ -599,7 +599,7 @@ describe( 'model test utils', () => { } ); it( 'sets selection within a text with different attributes', () => { - const result = parse( '<$text bold="true">fo[oba]r', document.schema, { + const result = parse( '<$text bold="true">fo[oba]r', document.schema, document.batch(), { selectionAttributes: { bold: true } } ); @@ -609,13 +609,13 @@ describe( 'model test utils', () => { it( 'throws when missing selection start', () => { expect( () => { - parse( 'foo]' ); + parse( 'foo]', document.schema, document.batch() ); } ).to.throw( Error ); } ); it( 'throws when missing selection end', () => { expect( () => { - parse( '[foo' ); + parse( '[foo', document.schema, document.batch() ); } ).to.throw( Error ); } ); } ); @@ -623,7 +623,7 @@ describe( 'model test utils', () => { function test( title, options ) { it( title, () => { const output = options.output || options.data; - const data = parse( options.data, document.schema ); + const data = parse( options.data, document.schema, document.batch() ); let model, selection; if ( data.selection && data.model ) { diff --git a/tests/model/document/document.js b/tests/model/document/document.js index b552c1c26..a698870c8 100644 --- a/tests/model/document/document.js +++ b/tests/model/document/document.js @@ -143,7 +143,7 @@ describe( 'Document', () => { expect( changeCallback.args[ 0 ][ 4 ] ).to.equal( delta.type ); } ); - it( 'should execute operation, fire event with proper data and not increase document version ' + + it( 'should execute operation, not fire event and not increase document version ' + 'when operation is not a document operation', () => { const changeCallback = sinon.spy(); const type = 't'; @@ -169,11 +169,7 @@ describe( 'Document', () => { expect( doc.history._deltas.length ).to.equal( 0 ); sinon.assert.calledOnce( operation._execute ); - sinon.assert.calledOnce( changeCallback ); - expect( changeCallback.args[ 0 ][ 1 ] ).to.equal( type ); - expect( changeCallback.args[ 0 ][ 2 ] ).to.equal( data ); - expect( changeCallback.args[ 0 ][ 3 ] ).to.deep.equal( batch ); - expect( changeCallback.args[ 0 ][ 4 ] ).to.equal( delta.type ); + sinon.assert.notCalled( changeCallback ); } ); it( 'should throw an error on the operation base version and the document version is different', () => { diff --git a/tests/model/schema/schema.js b/tests/model/schema/schema.js index d60ab29fb..9f89ca0a5 100644 --- a/tests/model/schema/schema.js +++ b/tests/model/schema/schema.js @@ -793,13 +793,6 @@ describe( 'Schema', () => { } ); it( 'should filter out disallowed attributes from given nodes', () => { - schema.removeDisallowedAttributes( [ text, image ], '$root' ); - - expect( Array.from( text.getAttributeKeys() ) ).to.deep.equal( [ 'a' ] ); - expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'b' ] ); - } ); - - it( 'should filter out disallowed attributes from given nodes (batch)', () => { const root = doc.getRoot(); const batch = doc.batch(); @@ -834,22 +827,6 @@ describe( 'Schema', () => { div = new Element( 'div', [], [ paragraph, bar, imageInDiv ] ); } ); - it( 'should filter out disallowed attributes from child nodes', () => { - schema.removeDisallowedAttributes( [ div ], '$root' ); - - expect( stringify( div ) ) - .to.equal( - '
' + - '' + - '<$text b="1">foo' + - '' + - '' + - '<$text a="1">bar' + - '' + - '
' - ); - } ); - it( 'should filter out disallowed attributes from child nodes (batch)', () => { const root = doc.getRoot(); const batch = doc.batch(); @@ -893,21 +870,21 @@ describe( 'Schema', () => { } ); it( 'should accept iterable as nodes', () => { - schema.removeDisallowedAttributes( frag.getChildren(), '$root' ); + schema.removeDisallowedAttributes( frag.getChildren(), '$root', doc.batch() ); expect( stringify( frag ) ) .to.equal( '<$text a="1">foo<$text b="1">barbiz' ); } ); it( 'should accept Position as inside', () => { - schema.removeDisallowedAttributes( frag.getChildren(), Position.createAt( root ) ); + schema.removeDisallowedAttributes( frag.getChildren(), Position.createAt( root ), doc.batch() ); expect( stringify( frag ) ) .to.equal( '<$text a="1">foo<$text b="1">barbiz' ); } ); it( 'should accept Node as inside', () => { - schema.removeDisallowedAttributes( frag.getChildren(), [ root ] ); + schema.removeDisallowedAttributes( frag.getChildren(), [ root ], doc.batch() ); expect( stringify( frag ) ) .to.equal( '<$text a="1">foo<$text b="1">barbiz' ); @@ -920,7 +897,7 @@ describe( 'Schema', () => { const image = new Element( 'image', { a: 1, b: 1 } ); - schema.removeDisallowedAttributes( [ image ], '$root' ); + schema.removeDisallowedAttributes( [ image ], '$root', doc.batch() ); expect( Array.from( image.getAttributeKeys() ) ).to.deep.equal( [ 'a', 'b' ] ); } ); diff --git a/tests/model/selection.js b/tests/model/selection.js index 5314efc26..ad7b2f399 100644 --- a/tests/model/selection.js +++ b/tests/model/selection.js @@ -894,14 +894,14 @@ describe( 'Selection', () => { } ); it( 'should return selected element', () => { - const { selection, model } = parse( '

foo

[

bar

]

baz

', schema ); + const { selection, model } = parse( '

foo

[

bar

]

baz

', schema, doc.batch() ); const p = model.getChild( 1 ); expect( selection.getSelectedElement() ).to.equal( p ); } ); it( 'should return null if there is more than one range', () => { - const { selection } = parse( '[

foo

][

bar

]

baz

', schema ); + const { selection } = parse( '[

foo

][

bar

]

baz

', schema, doc.batch() ); expect( selection.getSelectedElement() ).to.be.null; } ); @@ -911,13 +911,13 @@ describe( 'Selection', () => { } ); it( 'should return null if selection is not over single element #1', () => { - const { selection } = parse( '

foo

[

bar

baz}

', schema ); + const { selection } = parse( '

foo

[

bar

baz}

', schema, doc.batch() ); expect( selection.getSelectedElement() ).to.be.null; } ); it( 'should return null if selection is not over single element #2', () => { - const { selection } = parse( '

{bar}

', schema ); + const { selection } = parse( '

{bar}

', schema, doc.batch() ); expect( selection.getSelectedElement() ).to.be.null; } ); From b7d34becb80b80008040ea1514b0d1413dc495f6 Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Mon, 27 Nov 2017 17:51:42 +0100 Subject: [PATCH 37/44] Docs for batch methods. --- src/model/batch.js | 217 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 199 insertions(+), 18 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index 17492067e..a6286dff5 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -130,18 +130,77 @@ export default class Batch { } } + /** + * 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 ); @@ -185,6 +244,27 @@ export default class Batch { } } + /** + * 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 ); @@ -193,6 +273,27 @@ export default class Batch { } } + /** + * 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 ); @@ -201,10 +302,35 @@ export default class Batch { } } + /** + * 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} data 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' ); @@ -213,6 +339,16 @@ export default class Batch { } } + /** + * 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( text, attributes, parent ) { if ( attributes instanceof DocumentFragment || attributes instanceof Element ) { this.insert( this.createElement( text ), attributes, 'end' ); @@ -238,6 +374,19 @@ export default class Batch { } } + /** + * 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( range, { + * 'bold': true, + * 'italic': true + * } ); + * + * @param {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange + * Model item or range on which the attributes will be set. + * @param {Object} attributes Attributes keys and values. + */ setAttributes( itemOrRange, attributes ) { for ( const [ key, val ] of toMap( attributes ) ) { this.setAttribute( itemOrRange, key, val ); @@ -261,6 +410,12 @@ export default class Batch { } } + /** + * 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() ) { @@ -277,6 +432,30 @@ export default class Batch { } } + /** + * 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} for 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 ) ) { /** @@ -707,15 +886,13 @@ function setAttributeToRange( batch, key, value, range ) { } } -/** - * 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. - */ +// 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 ); @@ -748,15 +925,13 @@ function setAttributeToItem( batch, key, value, item ) { } } -/** - * 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. - */ +// 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(); @@ -768,6 +943,12 @@ function addMarkerOperation( batch, name, oldRange, newRange ) { doc.applyOperation( operation ); } +// Returns true if both elements are in the document or are the same element. +// +// Elements "in the same document" can be moved (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 it OT. This is because features like undo or +// collaboration may track changes on the document and should not get unexpected move. function isTheSameDocument( rootA, rootB ) { if ( rootA === rootB ) { return true; From 0484d42d53bc78fefa629af49a5e9083c3fb715e Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Mon, 27 Nov 2017 18:02:41 +0100 Subject: [PATCH 38/44] Renamed isTheSameDocument to isSameTree. --- src/model/batch.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index a6286dff5..3d110d96b 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -210,7 +210,7 @@ export default class Batch { // 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 ( isTheSameDocument( item.root, position.root ) ) { + if ( isSameTree( item.root, position.root ) ) { // If it's we just need to move it. this.move( Range.createOn( item ), position ); @@ -449,7 +449,7 @@ export default class Batch { * 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} for such cases. + * {@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 @@ -477,7 +477,7 @@ export default class Batch { const position = Position.createAt( itemOrPosition, offset ); - if ( !isTheSameDocument( range.root, position.root ) ) { + 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. @@ -943,13 +943,14 @@ function addMarkerOperation( batch, name, oldRange, newRange ) { doc.applyOperation( operation ); } -// Returns true if both elements are in the document or are the same element. +// Returns `true` if both root elements are the same element or both are documents root elements. // -// Elements "in the same document" can be moved (you can move element form one documents root to another, or +// 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 it OT. This is because features like undo or -// collaboration may track changes on the document and should not get unexpected move. -function isTheSameDocument( rootA, rootB ) { +// 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; } From bdf434cd9a19e3c62bd12d85257d7c0c78cb27af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 27 Nov 2017 21:22:52 +0100 Subject: [PATCH 39/44] Changed params order in Batch#setAttributes. --- src/conversion/buildviewconverter.js | 2 +- src/model/batch.js | 53 ++++----- src/model/documentselection.js | 10 +- src/model/schema.js | 2 +- tests/conversion/modelconversiondispatcher.js | 34 +++--- tests/manual/tickets/475/1.js | 2 +- tests/model/batch.js | 110 +++++++++--------- 7 files changed, 105 insertions(+), 108 deletions(-) diff --git a/src/conversion/buildviewconverter.js b/src/conversion/buildviewconverter.js index bb4a120e9..8efc18962 100644 --- a/src/conversion/buildviewconverter.js +++ b/src/conversion/buildviewconverter.js @@ -528,7 +528,7 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) { }; if ( conversionApi.schema.check( schemaQuery ) ) { - data.batch.setAttribute( toChange, attribute.key, attribute.value ); + data.batch.setAttribute( attribute.key, attribute.value, toChange ); } } diff --git a/src/model/batch.js b/src/model/batch.js index 3d110d96b..b8123a7ea 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -327,7 +327,7 @@ export default class Batch { * batch.appendText( 'foo', paragraph ); * batch.appendText( 'foo', { 'bold': true }, paragraph ); * - * @param {String} data Text data. + * @param {String} text Text data. * @param {Object} [attributes] Text attributes. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent */ @@ -349,11 +349,11 @@ export default class Batch { * @param {Object} [attributes] Elements attributes. * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} parent */ - appendElement( text, attributes, parent ) { + appendElement( name, attributes, parent ) { if ( attributes instanceof DocumentFragment || attributes instanceof Element ) { - this.insert( this.createElement( text ), attributes, 'end' ); + this.insert( this.createElement( name ), attributes, 'end' ); } else { - this.insert( this.createElement( text, attributes ), parent, 'end' ); + this.insert( this.createElement( name, attributes ), parent, 'end' ); } } @@ -361,12 +361,12 @@ export default class Batch { * 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 {module:engine/model/item~Item|module:engine/model/range~Range} itemOrRange - * Model item or range on which the attribute will be set. * @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( itemOrRange, key, value ) { + setAttribute( key, value, itemOrRange ) { if ( itemOrRange instanceof Range ) { setAttributeToRange( this, key, value, itemOrRange ); } else { @@ -378,18 +378,18 @@ export default class Batch { * 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( 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. - * @param {Object} attributes Attributes keys and values. */ - setAttributes( itemOrRange, attributes ) { + setAttributes( attributes, itemOrRange ) { for ( const [ key, val ] of toMap( attributes ) ) { - this.setAttribute( itemOrRange, key, val ); + this.setAttribute( key, val, itemOrRange ); } } @@ -397,12 +397,11 @@ export default class Batch { * 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. - * @method module:engine/model/batch~Batch#removeAttribute - * @param {String} key Attribute key. */ - removeAttribute( itemOrRange, key ) { + removeAttribute( key, itemOrRange ) { if ( itemOrRange instanceof Range ) { setAttributeToRange( this, key, null, itemOrRange ); } else { @@ -419,7 +418,7 @@ export default class Batch { clearAttributes( itemOrRange ) { const removeAttributesFromItem = item => { for ( const attribute of item.getAttributeKeys() ) { - this.removeAttribute( item, attribute ); + this.removeAttribute( attribute, item ); } }; @@ -819,18 +818,16 @@ export default class Batch { } } -/** - * 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. - */ +// 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; diff --git a/src/model/documentselection.js b/src/model/documentselection.js index 8b6375a4e..2b44043da 100644 --- a/src/model/documentselection.js +++ b/src/model/documentselection.js @@ -543,7 +543,7 @@ export default class DocumentSelection extends Selection { _removeStoredAttribute( key ) { const storeKey = DocumentSelection._getStoreAttributeKey( key ); - this._document.batch().removeAttribute( this.anchor.parent, storeKey ); + this._document.batch().removeAttribute( storeKey, this.anchor.parent ); } /** @@ -556,7 +556,7 @@ export default class DocumentSelection extends Selection { _storeAttribute( key, value ) { const storeKey = DocumentSelection._getStoreAttributeKey( key ); - this._document.batch().setAttribute( this.anchor.parent, storeKey, value ); + this._document.batch().setAttribute( storeKey, value, this.anchor.parent ); } /** @@ -572,13 +572,13 @@ export default class DocumentSelection extends Selection { for ( const [ oldKey ] of this._getStoredAttributes() ) { const storeKey = DocumentSelection._getStoreAttributeKey( oldKey ); - batch.removeAttribute( selectionParent, storeKey ); + batch.removeAttribute( storeKey, selectionParent ); } for ( const [ key, value ] of attrs ) { const storeKey = DocumentSelection._getStoreAttributeKey( key ); - batch.setAttribute( selectionParent, storeKey, value ); + batch.setAttribute( storeKey, value, selectionParent ); } } @@ -731,7 +731,7 @@ function clearAttributesStoredInElement( changes, batch, document ) { const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) ); for ( const key of storedAttributes ) { - batch.removeAttribute( changeParent, key ); + batch.removeAttribute( key, changeParent ); } } ); } diff --git a/src/model/schema.js b/src/model/schema.js index 1b454059a..a981620f3 100644 --- a/src/model/schema.js +++ b/src/model/schema.js @@ -432,7 +432,7 @@ export default class Schema { // TODO: this should be improved to check all combination of attributes. for ( const attribute of node.getAttributeKeys() ) { if ( !this.check( { name, attributes: attribute, inside: queryPath } ) ) { - batch.removeAttribute( node, attribute ); + batch.removeAttribute( attribute, node ); } } } diff --git a/tests/conversion/modelconversiondispatcher.js b/tests/conversion/modelconversiondispatcher.js index dac0eb408..cd3c3ba24 100644 --- a/tests/conversion/modelconversiondispatcher.js +++ b/tests/conversion/modelconversiondispatcher.js @@ -112,13 +112,13 @@ describe( 'ModelConversionDispatcher', () => { dispatcher.on( 'addAttribute:key:$text', cbAddText ); dispatcher.on( 'addAttribute:key:image', cbAddImage ); - doc.batch().setAttribute( image, 'key', 'value' ); + doc.batch().setAttribute( 'key', 'value', image ); // Callback for adding attribute on text not called. expect( cbAddText.called ).to.be.false; expect( cbAddImage.calledOnce ).to.be.true; - doc.batch().setAttribute( ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ), 'key', 'value' ); + doc.batch().setAttribute( 'key', 'value', ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ) ); expect( cbAddText.calledOnce ).to.be.true; // Callback for adding attribute on image not called this time. @@ -133,16 +133,16 @@ describe( 'ModelConversionDispatcher', () => { dispatcher.on( 'changeAttribute:key:$text', cbChangeText ); dispatcher.on( 'changeAttribute:key:image', cbChangeImage ); - batch.setAttribute( image, 'key', 'value' ); - batch.setAttribute( image, 'key', 'newValue' ); + batch.setAttribute( 'key', 'value', image ); + batch.setAttribute( 'key', 'newValue', image ); // Callback for adding attribute on text not called. expect( cbChangeText.called ).to.be.false; expect( cbChangeImage.calledOnce ).to.be.true; const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ); - batch.setAttribute( range, 'key', 'value' ); - batch.setAttribute( range, 'key', 'newValue' ); + batch.setAttribute( 'key', 'value', range ); + batch.setAttribute( 'key', 'newValue', range ); expect( cbChangeText.calledOnce ).to.be.true; // Callback for adding attribute on image not called this time. @@ -157,16 +157,16 @@ describe( 'ModelConversionDispatcher', () => { dispatcher.on( 'removeAttribute:key:$text', cbRemoveText ); dispatcher.on( 'removeAttribute:key:image', cbRemoveImage ); - batch.setAttribute( image, 'key', 'value' ); - batch.removeAttribute( image, 'key' ); + batch.setAttribute( 'key', 'value', image ); + batch.removeAttribute( 'key', image ); // Callback for adding attribute on text not called. expect( cbRemoveText.called ).to.be.false; expect( cbRemoveImage.calledOnce ).to.be.true; const range = ModelRange.createFromParentsAndOffsets( root, 3, root, 4 ); - batch.setAttribute( range, 'key', 'value' ); - batch.removeAttribute( range, 'key' ); + batch.setAttribute( 'key', 'value', range ); + batch.removeAttribute( 'key', range ); expect( cbRemoveText.calledOnce ).to.be.true; // Callback for adding attribute on image not called this time. @@ -187,7 +187,7 @@ describe( 'ModelConversionDispatcher', () => { const gyNode = new ModelElement( 'image' ); doc.graveyard.appendChildren( gyNode ); - doc.batch().setAttribute( gyNode, 'key', 'value' ); + doc.batch().setAttribute( 'key', 'value', gyNode ); expect( dispatcher.fire.called ).to.be.false; } ); @@ -623,8 +623,8 @@ describe( 'ModelConversionDispatcher', () => { doc.enqueueChanges( () => { const batch = doc.batch(); - batch.setAttribute( ModelRange.createIn( root ), 'bold', true ); - batch.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); + batch.setAttribute( 'bold', true, ModelRange.createIn( root ) ); + batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) ); } ); dispatcher.on( 'selection', ( evt, data, consumable ) => { @@ -642,8 +642,8 @@ describe( 'ModelConversionDispatcher', () => { doc.enqueueChanges( () => { const batch = doc.batch(); - batch.setAttribute( ModelRange.createIn( root ), 'bold', true ); - batch.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); + batch.setAttribute( 'bold', true, ModelRange.createIn( root ) ); + batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) ); } ); dispatcher.convertSelection( doc.selection, [] ); @@ -662,8 +662,8 @@ describe( 'ModelConversionDispatcher', () => { doc.enqueueChanges( () => { const batch = doc.batch(); - batch.setAttribute( ModelRange.createIn( root ), 'bold', true ); - batch.setAttribute( ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ), 'italic', true ); + batch.setAttribute( 'bold', true, ModelRange.createIn( root ) ); + batch.setAttribute( 'italic', true, ModelRange.createFromParentsAndOffsets( root, 4, root, 5 ) ); } ); dispatcher.convertSelection( doc.selection, [] ); diff --git a/tests/manual/tickets/475/1.js b/tests/manual/tickets/475/1.js index ecfe61995..f5c090c73 100644 --- a/tests/manual/tickets/475/1.js +++ b/tests/manual/tickets/475/1.js @@ -80,7 +80,7 @@ class AutoLinker extends Plugin { doc.enqueueChanges( () => { const urlRange = Range.createFromPositionAndShift( livePos, url.length ); - batch.setAttribute( urlRange, 'link', url ); + batch.setAttribute( 'link', url, urlRange ); } ); } } ); diff --git a/tests/model/batch.js b/tests/model/batch.js index 9fc9c49f1..34bf7968d 100644 --- a/tests/model/batch.js +++ b/tests/model/batch.js @@ -955,31 +955,31 @@ describe( 'Batch', () => { describe( 'setAttribute', () => { it( 'should create the attribute on element', () => { - batch.setAttribute( node, 'b', 2 ); + batch.setAttribute( 'b', 2, node ); expect( spy.callCount ).to.equal( 1 ); expect( node.getAttribute( 'b' ) ).to.equal( 2 ); } ); it( 'should change the attribute of element', () => { - batch.setAttribute( node, 'a', 2 ); + batch.setAttribute( 'a', 2, node ); expect( spy.callCount ).to.equal( 1 ); expect( node.getAttribute( 'a' ) ).to.equal( 2 ); } ); it( 'should create the attribute on text node', () => { - batch.setAttribute( text, 'b', 2 ); + batch.setAttribute( 'b', 2, text ); expect( spy.callCount ).to.equal( 1 ); expect( root.getChild( 1 ).getAttribute( 'b' ) ).to.equal( 2 ); } ); it( 'should change the attribute of text node', () => { - batch.setAttribute( text, 'a', 2 ); + batch.setAttribute( 'a', 2, text ); expect( spy.callCount ).to.equal( 1 ); expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.equal( 2 ); } ); it( 'should do nothing if the attribute value is the same', () => { - batch.setAttribute( node, 'a', 1 ); + batch.setAttribute( 'a', 1, node ); expect( spy.callCount ).to.equal( 0 ); expect( node.getAttribute( 'a' ) ).to.equal( 1 ); } ); @@ -987,19 +987,19 @@ describe( 'Batch', () => { describe( 'removeAttribute', () => { it( 'should remove the attribute from element', () => { - batch.removeAttribute( node, 'a' ); + batch.removeAttribute( 'a', node ); expect( spy.callCount ).to.equal( 1 ); expect( node.getAttribute( 'a' ) ).to.be.undefined; } ); it( 'should remove the attribute from character', () => { - batch.removeAttribute( text, 'a' ); + batch.removeAttribute( 'a', text ); expect( spy.callCount ).to.equal( 1 ); expect( root.getChild( 1 ).getAttribute( 'a' ) ).to.be.undefined; } ); it( 'should do nothing if the attribute is not set', () => { - batch.removeAttribute( node, 'b' ); + batch.removeAttribute( 'b', node ); expect( spy.callCount ).to.equal( 0 ); } ); } ); @@ -1054,42 +1054,42 @@ describe( 'Batch', () => { describe( 'setAttribute', () => { it( 'should set the attribute on the range', () => { - batch.setAttribute( getRange( 3, 6 ), 'a', 3 ); + batch.setAttribute( 'a', 3, getRange( 3, 6 ) ); expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 3 ); expect( getCompressedAttrs() ).to.equal( '111333111222---1112------' ); } ); it( 'should split the operations if parts of the range have different attributes', () => { - batch.setAttribute( getRange( 4, 14 ), 'a', 3 ); + batch.setAttribute( 'a', 3, getRange( 4, 14 ) ); expect( spy.callCount ).to.equal( 4 ); expect( getChangesAttrsCount() ).to.equal( 10 ); expect( getCompressedAttrs() ).to.equal( '111-3333333333-1112------' ); } ); it( 'should split the operations if parts of the part of the range have the attribute', () => { - batch.setAttribute( getRange( 4, 14 ), 'a', 2 ); + batch.setAttribute( 'a', 2, getRange( 4, 14 ) ); expect( spy.callCount ).to.equal( 3 ); expect( getChangesAttrsCount() ).to.equal( 7 ); expect( getCompressedAttrs() ).to.equal( '111-2222222222-1112------' ); } ); it( 'should strip the range if the beginning have the attribute', () => { - batch.setAttribute( getRange( 1, 5 ), 'a', 1 ); + batch.setAttribute( 'a', 1, getRange( 1, 5 ) ); expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '11111-111222---1112------' ); } ); it( 'should strip the range if the ending have the attribute', () => { - batch.setAttribute( getRange( 13, 17 ), 'a', 1 ); + batch.setAttribute( 'a', 1, getRange( 13, 17 ) ); expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '111---111222-111112------' ); } ); it( 'should do nothing if the range has attribute', () => { - batch.setAttribute( getRange( 0, 3 ), 'a', 1 ); + batch.setAttribute( 'a', 1, getRange( 0, 3 ) ); expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); @@ -1100,7 +1100,7 @@ describe( 'Batch', () => { new Position( root, [ 19 ] ) ); - batch.setAttribute( range, 'a', 1 ); + batch.setAttribute( 'a', 1, range ); expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112-11---' ); @@ -1112,7 +1112,7 @@ describe( 'Batch', () => { new Position( root, [ 21 ] ) ); - batch.setAttribute( range, 'a', 1 ); + batch.setAttribute( 'a', 1, range ); expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 4 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112-1111-' ); @@ -1124,19 +1124,19 @@ describe( 'Batch', () => { new Position( root, [ 19 ] ) ); - batch.setAttribute( range, 'a', 3 ); + batch.setAttribute( 'a', 3, range ); expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should not create an operation if is collapsed', () => { - batch.setAttribute( getRange( 3, 3 ), 'a', 1 ); + batch.setAttribute( 'a', 1, getRange( 3, 3 ) ); expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should create a proper operations for the mixed range', () => { - batch.setAttribute( getRange( 0, 20 ), 'a', 1 ); + batch.setAttribute( 'a', 1, getRange( 0, 20 ) ); expect( spy.callCount ).to.equal( 5 ); expect( getChangesAttrsCount() ).to.equal( 14 ); expect( getCompressedAttrs() ).to.equal( '11111111111111111111111--' ); @@ -1145,42 +1145,42 @@ describe( 'Batch', () => { describe( 'removeAttribute', () => { it( 'should remove the attribute on the range', () => { - batch.removeAttribute( getRange( 0, 2 ), 'a' ); + batch.removeAttribute( 'a', getRange( 0, 2 ) ); expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 2 ); expect( getCompressedAttrs() ).to.equal( '--1---111222---1112------' ); } ); it( 'should split the operations if parts of the range have different attributes', () => { - batch.removeAttribute( getRange( 7, 11 ), 'a' ); + batch.removeAttribute( 'a', getRange( 7, 11 ) ); expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 4 ); expect( getCompressedAttrs() ).to.equal( '111---1----2---1112------' ); } ); it( 'should split the operations if parts of the part of the range have no attribute', () => { - batch.removeAttribute( getRange( 1, 7 ), 'a' ); + batch.removeAttribute( 'a', getRange( 1, 7 ) ); expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 3 ); expect( getCompressedAttrs() ).to.equal( '1------11222---1112------' ); } ); it( 'should strip the range if the beginning have no attribute', () => { - batch.removeAttribute( getRange( 4, 12 ), 'a' ); + batch.removeAttribute( 'a', getRange( 4, 12 ) ); expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 6 ); expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); } ); it( 'should strip the range if the ending have no attribute', () => { - batch.removeAttribute( getRange( 7, 15 ), 'a' ); + batch.removeAttribute( 'a', getRange( 7, 15 ) ); expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 5 ); expect( getCompressedAttrs() ).to.equal( '111---1--------1112------' ); } ); it( 'should do nothing if the range has no attribute', () => { - batch.removeAttribute( getRange( 4, 5 ), 'a' ); + batch.removeAttribute( 'a', getRange( 4, 5 ) ); expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); @@ -1191,27 +1191,27 @@ describe( 'Batch', () => { new Position( root, [ 19 ] ) ); - batch.removeAttribute( range, 'a' ); + batch.removeAttribute( 'a', range ); expect( spy.callCount ).to.equal( 0 ); expect( getChangesAttrsCount() ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should not apply operation twice in the range contains opening and closing tags', () => { - batch.removeAttribute( getRange( 18, 22 ), 'a' ); + batch.removeAttribute( 'a', getRange( 18, 22 ) ); expect( spy.callCount ).to.equal( 1 ); expect( getChangesAttrsCount() ).to.equal( 1 ); expect( getCompressedAttrs() ).to.equal( '111---111222---111-------' ); } ); it( 'should not create an operation if range is collapsed', () => { - batch.removeAttribute( getRange( 3, 3 ), 'a' ); + batch.removeAttribute( 'a', getRange( 3, 3 ) ); expect( spy.callCount ).to.equal( 0 ); expect( getCompressedAttrs() ).to.equal( '111---111222---1112------' ); } ); it( 'should create a proper operations for the mixed range', () => { - batch.removeAttribute( getRange( 3, 15 ), 'a' ); + batch.removeAttribute( 'a', getRange( 3, 15 ) ); expect( spy.callCount ).to.equal( 2 ); expect( getChangesAttrsCount() ).to.equal( 6 ); expect( getCompressedAttrs() ).to.equal( '111------------1112------' ); @@ -1229,41 +1229,41 @@ describe( 'Batch', () => { describe( 'setAttribute', () => { it( 'should create the attribute on root', () => { - batch.setAttribute( root, 'b', 2 ); + batch.setAttribute( 'b', 2, root ); expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'b' ) ).to.equal( 2 ); } ); it( 'should create the attribute on detached root', () => { - batch.setAttribute( p, 'b', 2 ); + batch.setAttribute( 'b', 2, p ); expect( spy.callCount ).to.equal( 1 ); expect( p.getAttribute( 'b' ) ).to.equal( 2 ); } ); it( 'should change the attribute of root', () => { - batch.setAttribute( root, 'a', 2 ); + batch.setAttribute( 'a', 2, root ); expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'a' ) ).to.equal( 2 ); } ); it( 'should change the attribute of detached root', () => { - batch.setAttribute( p, 'a', 2 ); + batch.setAttribute( 'a', 2, p ); expect( spy.callCount ).to.equal( 1 ); expect( p.getAttribute( 'a' ) ).to.equal( 2 ); } ); it( 'should do nothing if the attribute value is the same', () => { - batch.setAttribute( root, 'a', 1 ); + batch.setAttribute( 'a', 1, root ); expect( spy.callCount ).to.equal( 1 ); - batch.setAttribute( root, 'a', 1 ); + batch.setAttribute( 'a', 1, root ); expect( spy.callCount ).to.equal( 1 ); expect( root.getAttribute( 'a' ) ).to.equal( 1 ); } ); it( 'should do nothing if the attribute value is the same on detached root', () => { - batch.setAttribute( p, 'a', 1 ); + batch.setAttribute( 'a', 1, p ); expect( spy.callCount ).to.equal( 1 ); - batch.setAttribute( p, 'a', 1 ); + batch.setAttribute( 'a', 1, p ); expect( spy.callCount ).to.equal( 1 ); expect( p.getAttribute( 'a' ) ).to.equal( 1 ); } ); @@ -1271,15 +1271,15 @@ describe( 'Batch', () => { describe( 'removeAttribute', () => { it( 'should remove the attribute from root', () => { - batch.setAttribute( root, 'a', 1 ); - batch.removeAttribute( root, 'a' ); + batch.setAttribute( 'a', 1, root ); + batch.removeAttribute( 'a', root ); expect( spy.callCount ).to.equal( 2 ); expect( root.getAttribute( 'a' ) ).to.be.undefined; } ); it( 'should do nothing if the attribute is not set', () => { - batch.removeAttribute( root, 'b' ); + batch.removeAttribute( 'b', root ); expect( spy.callCount ).to.equal( 0 ); } ); } ); @@ -1319,7 +1319,7 @@ describe( 'Batch', () => { } ); it( 'should clear attributes on root element', () => { - batch.setAttributes( root, { a: 1, b: 2, c: 3 } ); + batch.setAttributes( { a: 1, b: 2, c: 3 }, root ); expect( Array.from( root.getAttributeKeys() ).length ).to.equal( 3 ); @@ -1345,11 +1345,11 @@ describe( 'Batch', () => { const nodeB = new Element( 'p', { b: 2 } ); root.insertChildren( 0, [ nodeA, nodeB ] ); - batch.setAttribute( nodeA, 'a', 1 ); + batch.setAttribute( 'a', 1, nodeA ); expect( batch.deltas.length ).to.equal( 0 ); - batch.removeAttribute( Range.createIn( root ), 'x' ); + batch.removeAttribute( 'x', Range.createIn( root ) ); expect( batch.deltas.length ).to.equal( 0 ); } ); @@ -1376,7 +1376,7 @@ describe( 'Batch', () => { // such a big amount of the same tests, so let's use a spy here. const spy = sinon.spy( batch, 'setAttribute' ); - batch.setAttributes( range, { a: 3, c: null } ); + batch.setAttributes( { a: 3, c: null }, range ); // Verify result. expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] ); @@ -1384,8 +1384,8 @@ describe( 'Batch', () => { // Verify operations sinon.assert.calledTwice( spy ); - sinon.assert.calledWith( spy.firstCall, range, 'a', 3 ); - sinon.assert.calledWith( spy.secondCall, range, 'c', null ); + sinon.assert.calledWith( spy.firstCall, 'a', 3, range ); + sinon.assert.calledWith( spy.secondCall, 'c', null, range ); } ); it( 'should set attributes one by one on range for map as attributes list', () => { @@ -1395,7 +1395,7 @@ describe( 'Batch', () => { // such a big amount of the same tests, so let's use a spy here. const spy = sinon.spy( batch, 'setAttribute' ); - batch.setAttributes( range, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) ); + batch.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), range ); // Verify result. expect( Array.from( frag.getChild( 0 ).getAttributes() ) ).to.deep.equal( [ [ 'a', 3 ] ] ); @@ -1403,8 +1403,8 @@ describe( 'Batch', () => { // Verify operations sinon.assert.calledTwice( spy ); - sinon.assert.calledWith( spy.firstCall, range, 'a', 3 ); - sinon.assert.calledWith( spy.secondCall, range, 'c', null ); + sinon.assert.calledWith( spy.firstCall, 'a', 3, range ); + sinon.assert.calledWith( spy.secondCall, 'c', null, range ); } ); it( 'should set attributes one by one on item', () => { @@ -1412,15 +1412,15 @@ describe( 'Batch', () => { // such a big amount of the same tests, so let's use a spy here. const spy = sinon.spy( batch, 'setAttribute' ); - batch.setAttributes( item, { a: 3, c: null } ); + batch.setAttributes( { a: 3, c: null }, item ); // Verify result. expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] ); // Verify operations sinon.assert.calledTwice( spy ); - sinon.assert.calledWith( spy.firstCall, item, 'a', 3 ); - sinon.assert.calledWith( spy.secondCall, item, 'c', null ); + sinon.assert.calledWith( spy.firstCall, 'a', 3, item ); + sinon.assert.calledWith( spy.secondCall, 'c', null, item ); } ); it( 'should set attributes one by one on item for maps as attributes list', () => { @@ -1428,15 +1428,15 @@ describe( 'Batch', () => { // such a big amount of the same tests, so let's use a spy here. const spy = sinon.spy( batch, 'setAttribute' ); - batch.setAttributes( item, new Map( [ [ 'a', 3 ], [ 'c', null ] ] ) ); + batch.setAttributes( new Map( [ [ 'a', 3 ], [ 'c', null ] ] ), item ); // Verify result. expect( Array.from( item.getAttributes() ) ).to.deep.equal( [ [ 'b', 2 ], [ 'a', 3 ] ] ); // Verify operations sinon.assert.calledTwice( spy ); - sinon.assert.calledWith( spy.firstCall, item, 'a', 3 ); - sinon.assert.calledWith( spy.secondCall, item, 'c', null ); + sinon.assert.calledWith( spy.firstCall, 'a', 3, item ); + sinon.assert.calledWith( spy.secondCall, 'c', null, item ); } ); } ); From d29d6e9dc40a0294e399fc40fbd10448bad7df80 Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Tue, 28 Nov 2017 13:33:45 +0100 Subject: [PATCH 40/44] Fixes in Docs. --- src/model/batch.js | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/model/batch.js b/src/model/batch.js index b8123a7ea..a73cfce38 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -47,14 +47,14 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; * * For example to create two separate undo steps you can call: * - * doc.batch().insert( firstPosition, 'foo' ); - * doc.batch().insert( secondPosition, 'bar' ); + * doc.batch().insert( 'foo', firstPosition ); + * doc.batch().insert( 'bar', secondPosition ); * * To create a single undo step: * * const batch = doc.batch(); - * batch.insert( firstPosition, 'foo' ); - * batch.insert( secondPosition, 'bar' ); + * batch.insert( 'foo', firstPosition ); + * batch.insert( 'bar', secondPosition ); * */ export default class Batch { @@ -134,7 +134,7 @@ export default class Batch { * Creates a new {@link module:engine/model/text~Text text node}. * * batch.createText( 'foo' ); - * batch.createText( 'foo', { bold: true } ); + * batch.createText( 'foo', { 'bold': true } ); * * @param {String} data Text data. * @param {Object} [attributes] Text attributes. @@ -178,22 +178,22 @@ export default class Batch { * const text = batch.createText( 'foo' ); * batch.insert( text, paragraph, 5 ); * - * You can also use 'end' instead of the offset to insert at the end: + * 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' ); + * const paragraph = batch.createElement( 'paragraph' ); + * batch.insert( paragraph, anotherParagraph, 'after' ); * - * These parameters works the same way as {@link module:engine/model/position~Position#createAt}. + * 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}. + * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move move}. * * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} * item Item or document fragment to insert. @@ -253,11 +253,11 @@ export default class Batch { * 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', paragraph, 5 ); // inserts in paragraph, at offset 5 + * batch.insertText( 'foo', paragraph, 'end' ); // inserts 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}. + * These parameters works the same way as {@link module:engine/model/position~Position.createAt}. * * @param {String} data Text data. * @param {Object} [attributes] Text attributes. @@ -282,11 +282,11 @@ export default class Batch { * 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', paragraph, 5 ); // inserts in paragraph, at offset 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}. + * 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. @@ -311,7 +311,7 @@ export default class Batch { * 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}. + * {@link module:engine/model/item~Item item} use {@link module:engine/model/batch~Batch#move move}. * * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} * item Item or document fragment to insert. @@ -443,7 +443,7 @@ export default class Batch { * 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}. + * 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}, @@ -614,8 +614,8 @@ export default class Batch { /** * 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. + * The element needs to have a parent. It cannot be a root element nor document fragment. + * 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. */ @@ -659,6 +659,7 @@ export default class Batch { /** * 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. From 2df57cc28ada73bf1f5c655d7baab976d5729dcb Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Tue, 28 Nov 2017 14:16:35 +0100 Subject: [PATCH 41/44] Improve docs for isSameTree. --- src/model/batch.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/model/batch.js b/src/model/batch.js index a73cfce38..94ea16f91 100644 --- a/src/model/batch.js +++ b/src/model/batch.js @@ -949,9 +949,15 @@ function addMarkerOperation( batch, name, oldRange, newRange ) { // 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; } - return rootA instanceof RootElement && rootB instanceof RootElement; + // 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; } From 233fa44769f14c8f4d24d71ff5e71ef49d1dadf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 28 Nov 2017 14:51:42 +0100 Subject: [PATCH 42/44] Changed the way of checking if operaton is a document operation. --- src/model/operation/attributeoperation.js | 12 ++++------ src/model/operation/detachoperation.js | 12 ++++------ src/model/operation/insertoperation.js | 12 ++++------ src/model/operation/markeroperation.js | 11 +++++++-- src/model/operation/moveoperation.js | 20 +++++++++------- src/model/operation/nooperation.js | 19 +++++++++------ src/model/operation/operation.js | 3 +++ src/model/operation/reinsertoperation.js | 24 ++++++++++++------- src/model/operation/removeoperation.js | 24 ++++++++++++------- src/model/operation/renameoperation.js | 12 ++++------ src/model/operation/rootattributeoperation.js | 12 ++++------ 11 files changed, 90 insertions(+), 71 deletions(-) diff --git a/src/model/operation/attributeoperation.js b/src/model/operation/attributeoperation.js index 52dc3bff0..cb57426fb 100644 --- a/src/model/operation/attributeoperation.js +++ b/src/model/operation/attributeoperation.js @@ -73,6 +73,11 @@ export default class AttributeOperation extends Operation { * @member {*} */ this.newValue = newValue === undefined ? null : newValue; + + /** + * @inheritDoc + */ + this.isDocumentOperation = !!this.range.root.document; } /** @@ -88,13 +93,6 @@ export default class AttributeOperation extends Operation { } } - /** - * @inheritDoc - */ - get isDocumentOperation() { - return !!this.range.root.document; - } - /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/src/model/operation/detachoperation.js b/src/model/operation/detachoperation.js index c6a654a70..23110f0b9 100644 --- a/src/model/operation/detachoperation.js +++ b/src/model/operation/detachoperation.js @@ -45,6 +45,11 @@ export default class DetachOperation extends Operation { * @member {Number} #howMany */ this.howMany = howMany; + + /** + * @inheritDoc + */ + this.isDocumentOperation = false; } /** @@ -54,13 +59,6 @@ export default class DetachOperation extends Operation { return 'detach'; } - /** - * @inheritDoc - */ - get isDocumentOperation() { - return false; - } - /** * @inheritDoc */ diff --git a/src/model/operation/insertoperation.js b/src/model/operation/insertoperation.js index e8077e91c..7806be1ea 100644 --- a/src/model/operation/insertoperation.js +++ b/src/model/operation/insertoperation.js @@ -46,6 +46,11 @@ export default class InsertOperation extends Operation { * @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList */ this.nodes = new NodeList( normalizeNodes( nodes ) ); + + /** + * @inheritDoc + */ + this.isDocumentOperation = !!this.position.root.document; } /** @@ -55,13 +60,6 @@ export default class InsertOperation extends Operation { return 'insert'; } - /** - * @inheritDoc - */ - get isDocumentOperation() { - return !!this.position.root.document; - } - /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/src/model/operation/markeroperation.js b/src/model/operation/markeroperation.js index d7991d3e5..4c610ccd1 100644 --- a/src/model/operation/markeroperation.js +++ b/src/model/operation/markeroperation.js @@ -55,6 +55,11 @@ export default class MarkerOperation extends Operation { * @member {module:engine/model/markercollection~MarkerCollection} */ this._markers = markers; + + /** + * @inheritDoc + */ + this.isDocumentOperation = this._isDocumentOperation(); } /** @@ -65,9 +70,11 @@ export default class MarkerOperation extends Operation { } /** - * @inheritDoc + * Checks if operation is executed on document or document fragment nodes. + * + * @private */ - get isDocumentOperation() { + _isDocumentOperation() { if ( this.newRange ) { return !!this.newRange.root.document; } diff --git a/src/model/operation/moveoperation.js b/src/model/operation/moveoperation.js index cededd45a..1e04c81ec 100644 --- a/src/model/operation/moveoperation.js +++ b/src/model/operation/moveoperation.js @@ -64,6 +64,17 @@ export default class MoveOperation extends Operation { * @member {Boolean} module:engine/model/operation/moveoperation~MoveOperation#isSticky */ this.isSticky = false; + + /** + * Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}. + * + * Note that range cannot be moved within different documents e.g. from docFrag to document root so + * root of source and target positions is always the same. + * + * @readonly + * @member {Boolean} #isDocumentOperation + */ + this.isDocumentOperation = !!this.targetPosition.root.document; } /** @@ -73,15 +84,6 @@ export default class MoveOperation extends Operation { return 'move'; } - /** - * @inheritDoc - */ - get isDocumentOperation() { - // Note that range cannot be moved within different documents e.g. from docFrag to document root so - // root of source and target positions will be always the same. - return !!this.targetPosition.root.document; - } - /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/src/model/operation/nooperation.js b/src/model/operation/nooperation.js index a79b37544..ca238e3e0 100644 --- a/src/model/operation/nooperation.js +++ b/src/model/operation/nooperation.js @@ -20,6 +20,18 @@ import Operation from './operation'; * @extends module:engine/model/operation/operation~Operation */ export default class NoOperation extends Operation { + /** + * @inheritDoc + */ + constructor( baseVersion ) { + super( baseVersion ); + + /** + * @inheritDoc + */ + this.isDocumentOperation = true; + } + get type() { return 'noop'; } @@ -42,13 +54,6 @@ export default class NoOperation extends Operation { return new NoOperation( this.baseVersion + 1 ); } - /** - * @inheritDoc - */ - get isDocumentOperation() { - return true; - } - /** * @inheritDoc */ diff --git a/src/model/operation/operation.js b/src/model/operation/operation.js index b5aa08a71..43dc7fa79 100644 --- a/src/model/operation/operation.js +++ b/src/model/operation/operation.js @@ -97,6 +97,9 @@ export default class Operation { // Remove parent delta to avoid circular dependencies. delete json.delta; + // Only document operations are shared with other clients so it is not necessary to keep this information. + delete json.isDocumentOperation; + return json; } diff --git a/src/model/operation/reinsertoperation.js b/src/model/operation/reinsertoperation.js index edc3304dc..34ed54572 100644 --- a/src/model/operation/reinsertoperation.js +++ b/src/model/operation/reinsertoperation.js @@ -18,6 +18,21 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; * and fires different change event. */ export default class ReinsertOperation extends MoveOperation { + /** + * @inheritDocs + */ + constructor( sourcePosition, howMany, targetPosition, baseVersion ) { + super( sourcePosition, howMany, targetPosition, baseVersion ); + + /** + * Reinsert operation is always executed on attached items. + * + * @readonly + * @member {Boolean} + */ + this.isDocumentOperation = true; + } + /** * Position where nodes will be re-inserted. * @@ -41,15 +56,6 @@ export default class ReinsertOperation extends MoveOperation { return 'reinsert'; } - /** - * Reinsert operation is always executed on attached items. - * - * @member {Boolean} - */ - get isDocumentOperation() { - return true; - } - /** * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. * diff --git a/src/model/operation/removeoperation.js b/src/model/operation/removeoperation.js index 32ad7b6d2..6142d84db 100644 --- a/src/model/operation/removeoperation.js +++ b/src/model/operation/removeoperation.js @@ -16,20 +16,26 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; */ export default class RemoveOperation extends MoveOperation { /** - * @inheritDoc + * @inheritDocs */ - get type() { - return 'remove'; + constructor( sourcePosition, howMany, targetPosition, baseVersion ) { + super( sourcePosition, howMany, targetPosition, baseVersion ); + + /** + * Remove operation cannot be applied on element that is not inside the document + * so this will always be a document operation. + * + * @readonly + * @member {Boolean} + */ + this.isDocumentOperation = true; } /** - * Remove operation cannot be applied on element that is not inside the document - * so this will always be a document operation. - * - * @member {Boolean} + * @inheritDoc */ - get isDocumentOperation() { - return true; + get type() { + return 'remove'; } /** diff --git a/src/model/operation/renameoperation.js b/src/model/operation/renameoperation.js index bb39e6508..7f7334a4f 100644 --- a/src/model/operation/renameoperation.js +++ b/src/model/operation/renameoperation.js @@ -51,6 +51,11 @@ export default class RenameOperation extends Operation { * @member {String} module:engine/model/operation/renameoperation~RenameOperation#newName */ this.newName = newName; + + /** + * @inheritDoc + */ + this.isDocumentOperation = !!this.position.root.document; } /** @@ -60,13 +65,6 @@ export default class RenameOperation extends Operation { return 'rename'; } - /** - * @inheritDoc - */ - get isDocumentOperation() { - return !!this.position.root.document; - } - /** * Creates and returns an operation that has the same parameters as this operation. * diff --git a/src/model/operation/rootattributeoperation.js b/src/model/operation/rootattributeoperation.js index b29202491..606efe04e 100644 --- a/src/model/operation/rootattributeoperation.js +++ b/src/model/operation/rootattributeoperation.js @@ -67,6 +67,11 @@ export default class RootAttributeOperation extends Operation { * @member {*} */ this.newValue = newValue; + + /** + * @inheritDoc + */ + this.isDocumentOperation = !!this.root.document; } /** @@ -82,13 +87,6 @@ export default class RootAttributeOperation extends Operation { } } - /** - * @inheritDoc - */ - get isDocumentOperation() { - return !!this.root.document; - } - /** * Creates and returns an operation that has the same parameters as this operation. * From 84266ed5c0de09c78e8d0c7d9a39a3bcab569a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 28 Nov 2017 15:26:55 +0100 Subject: [PATCH 43/44] Moved batch from conversion additionalData do conversionApi. --- src/controller/datacontroller.js | 2 +- src/conversion/buildviewconverter.js | 8 +-- src/conversion/view-to-model-converters.js | 2 +- src/conversion/viewconversiondispatcher.js | 19 +++--- src/dev-utils/model.js | 2 +- tests/conversion/advanced-converters.js | 36 ++++++------ tests/conversion/buildviewconverter.js | 61 ++++++++++---------- tests/conversion/view-to-model-converters.js | 16 ++--- tests/conversion/viewconversiondispatcher.js | 28 ++++----- 9 files changed, 90 insertions(+), 84 deletions(-) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index 7443f4c17..9da0827a6 100644 --- a/src/controller/datacontroller.js +++ b/src/controller/datacontroller.js @@ -238,7 +238,7 @@ export default class DataController { * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment. */ toModel( viewElementOrFragment, batch, context = '$root' ) { - return this.viewToModel.convert( viewElementOrFragment, { context: [ context ], batch } ); + return this.viewToModel.convert( viewElementOrFragment, batch, { context: [ context ] } ); } /** diff --git a/src/conversion/buildviewconverter.js b/src/conversion/buildviewconverter.js index 8efc18962..373510d1c 100644 --- a/src/conversion/buildviewconverter.js +++ b/src/conversion/buildviewconverter.js @@ -270,7 +270,7 @@ class ViewConverterBuilder { toElement( element ) { function eventCallbackGen( from ) { return ( evt, data, consumable, conversionApi ) => { - const batch = data.batch; + const batch = conversionApi.batch; // There is one callback for all patterns in the matcher. // This will be usually just one pattern but we support matchers with many patterns too. @@ -434,8 +434,8 @@ class ViewConverterBuilder { */ toMarker( creator ) { function eventCallbackGen( from ) { - return ( evt, data, consumable ) => { - const batch = data.batch; + return ( evt, data, consumable, conversionApi ) => { + const batch = conversionApi.batch; // There is one callback for all patterns in the matcher. // This will be usually just one pattern but we support matchers with many patterns too. @@ -528,7 +528,7 @@ function setAttributeOn( toChange, attribute, data, conversionApi ) { }; if ( conversionApi.schema.check( schemaQuery ) ) { - data.batch.setAttribute( attribute.key, attribute.value, toChange ); + conversionApi.batch.setAttribute( attribute.key, attribute.value, toChange ); } } diff --git a/src/conversion/view-to-model-converters.js b/src/conversion/view-to-model-converters.js index 0652c82c2..f76ea6217 100644 --- a/src/conversion/view-to-model-converters.js +++ b/src/conversion/view-to-model-converters.js @@ -48,7 +48,7 @@ export function convertText() { if ( conversionApi.schema.check( schemaQuery ) ) { if ( consumable.consume( data.input ) ) { - data.output = data.batch.createText( data.input.data ); + data.output = conversionApi.batch.createText( data.input.data ); } } }; diff --git a/src/conversion/viewconversiondispatcher.js b/src/conversion/viewconversiondispatcher.js index 32da4824f..6b1f8a101 100644 --- a/src/conversion/viewconversiondispatcher.js +++ b/src/conversion/viewconversiondispatcher.js @@ -16,7 +16,6 @@ import ModelDocumentFragment from '../model/documentfragment'; import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; -import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend'; import log from '@ckeditor/ckeditor5-utils/src/log'; /** @@ -92,7 +91,7 @@ import log from '@ckeditor/ckeditor5-utils/src/log'; * // Fire conversion. * // Always take care where the converted model structure will be appended to. If this `viewDocumentFragment` * // is going to be appended directly to a '$root' element, use that in `context`. - * viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } ); + * viewDispatcher.convert( viewDocumentFragment, batch, { context: [ '$root' ] } ); * * Before each conversion process, `ViewConversionDispatcher` fires {@link ~ViewConversionDispatcher#event:viewCleanup} * event which can be used to prepare tree view for conversion. @@ -117,12 +116,15 @@ export default class ViewConversionDispatcher { * * @member {module:engine/conversion/viewconversiondispatcher~ViewConversionApi} */ - this.conversionApi = extend( {}, conversionApi ); + this.conversionApi = Object.assign( {}, conversionApi ); // `convertItem` and `convertChildren` are bound to this `ViewConversionDispatcher` instance and // set on `conversionApi`. This way only a part of `ViewConversionDispatcher` API is exposed. this.conversionApi.convertItem = this._convertItem.bind( this ); this.conversionApi.convertChildren = this._convertChildren.bind( this ); + + // Batch used for conversion. Is passed to #convert method and removed at the and of the conversion. + this.conversionApi.batch = null; } /** @@ -133,15 +135,16 @@ export default class ViewConversionDispatcher { * @fires documentFragment * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem * Part of the view to be converted. + * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. * @param {Object} additionalData Additional data to be passed in `data` argument when firing `ViewConversionDispatcher` * events. See also {@link ~ViewConversionDispatcher#event:element element event}. - * @param {module:engine/model/batch~Batch} additionalData.batch Batch to which the deltas will be added. * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}. */ - convert( viewItem, additionalData ) { - const batch = additionalData.batch; + convert( viewItem, batch, additionalData ) { + // Store batch in current conversion as conversionApi, will be removed at the end of this conversion. + this.conversionApi.batch = batch; this.fire( 'viewCleanup', viewItem ); @@ -173,7 +176,7 @@ export default class ViewConversionDispatcher { * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertItem */ _convertItem( input, consumable, additionalData = {} ) { - const data = extend( {}, additionalData, { + const data = Object.assign( {}, additionalData, { input, output: null } ); @@ -209,7 +212,7 @@ export default class ViewConversionDispatcher { * @see module:engine/conversion/viewconversiondispatcher~ViewConversionApi#convertChildren */ _convertChildren( input, consumable, additionalData ) { - const batch = additionalData.batch; + const batch = this.conversionApi.batch; const documentFragment = batch.createDocumentFragment(); for ( const viewChild of Array.from( input.getChildren() ) ) { diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 2c8103f48..295160bff 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -284,7 +284,7 @@ export function parse( data, schema, batch, options = {} ) { viewToModel.on( 'text', convertToModelText() ); // Convert view to model. - let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ], batch } ); + let model = viewToModel.convert( viewDocumentFragment.root, batch, { context: options.context || [ '$root' ] } ); // If root DocumentFragment contains only one element - return that element. if ( model.is( 'documentFragment' ) && model.childCount == 1 ) { diff --git a/tests/conversion/advanced-converters.js b/tests/conversion/advanced-converters.js index 9cccf90e7..c2e5659ec 100644 --- a/tests/conversion/advanced-converters.js +++ b/tests/conversion/advanced-converters.js @@ -208,8 +208,8 @@ describe( 'advanced-converters', () => { const viewFigureConverter = function( evt, data, consumable, conversionApi ) { if ( consumable.consume( data.input, { name: true } ) ) { - const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable, { batch } ); - const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable, { batch } ); + const modelImage = conversionApi.convertItem( data.input.getChild( 0 ), consumable, batch ); + const modelCaption = conversionApi.convertItem( data.input.getChild( 1 ), consumable, batch ); modelImage.appendChildren( modelCaption ); @@ -232,7 +232,7 @@ describe( 'advanced-converters', () => { const viewFigcaptionConverter = function( evt, data, consumable, conversionApi ) { if ( consumable.consume( data.input, { name: true } ) ) { const modelCaption = new ModelElement( 'caption' ); - const children = conversionApi.convertChildren( data.input, consumable, { batch } ); + const children = conversionApi.convertChildren( data.input, consumable, batch ); modelCaption.appendChildren( children ); @@ -287,7 +287,7 @@ describe( 'advanced-converters', () => { it( 'should convert view image to model', () => { const viewElement = new ViewContainerElement( 'img', { src: 'bar.jpg', title: 'bar' } ); - const modelElement = viewDispatcher.convert( viewElement, { batch } ); + const modelElement = viewDispatcher.convert( viewElement, batch ); expect( modelToString( modelElement ) ).to.equal( '' ); } ); @@ -301,7 +301,7 @@ describe( 'advanced-converters', () => { new ViewContainerElement( 'figcaption', null, new ViewText( 'foobar' ) ) ] ); - const modelElement = viewDispatcher.convert( viewElement, { batch } ); + const modelElement = viewDispatcher.convert( viewElement, batch ); expect( modelToString( modelElement ) ).to.equal( '' ); } ); @@ -372,7 +372,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); + data.output = conversionApi.convertChildren( data.input, consumable, batch ); } for ( const child of data.output ) { @@ -384,7 +384,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { attribute: 'title' } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); + data.output = conversionApi.convertChildren( data.input, consumable, batch ); } for ( const child of data.output ) { @@ -469,7 +469,7 @@ describe( 'advanced-converters', () => { } } - const children = conversionApi.convertChildren( data.input, consumable, { batch } ); + const children = conversionApi.convertChildren( data.input, consumable, batch ); data.output.appendChildren( children ); } } ); @@ -520,7 +520,7 @@ describe( 'advanced-converters', () => { it( 'should convert a view element to model', () => { const viewElement = new ViewAttributeElement( 'a', { href: 'foo.html', title: 'Foo title' }, new ViewText( 'foo' ) ); - const modelText = viewDispatcher.convert( viewElement, { batch } ).getChild( 0 ); + const modelText = viewDispatcher.convert( viewElement, batch ).getChild( 0 ); expect( modelText ).to.be.instanceof( ModelText ); expect( modelText.data ).to.equal( 'foo' ); @@ -591,7 +591,7 @@ describe( 'advanced-converters', () => { ] ); - const modelElement = viewDispatcher.convert( viewElement, { batch } ); + const modelElement = viewDispatcher.convert( viewElement, batch ); expect( modelToString( modelElement ) ).to.equal( 'foo' ); } ); @@ -603,7 +603,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:a', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true, attribute: 'href' } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); + data.output = conversionApi.convertChildren( data.input, consumable, batch ); } for ( const child of data.output ) { @@ -616,7 +616,7 @@ describe( 'advanced-converters', () => { if ( consumable.consume( data.input, { name: true } ) ) { data.output = new ModelElement( 'paragraph' ); - const children = conversionApi.convertChildren( data.input, consumable, { batch } ); + const children = conversionApi.convertChildren( data.input, consumable, batch ); for ( let i = 1; i < children.childCount; i++ ) { const child = children.getChild( i ); @@ -633,13 +633,13 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:table', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true } ) ) { - data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); + data.output = conversionApi.convertChildren( data.input, consumable, batch ); } } ); viewDispatcher.on( 'element:td', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true } ) ) { - data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); + data.output = conversionApi.convertChildren( data.input, consumable, batch ); } } ); @@ -654,7 +654,7 @@ describe( 'advanced-converters', () => { ] ) ] ); - const model = viewDispatcher.convert( viewTable, { batch } ); + const model = viewDispatcher.convert( viewTable, batch ); const modelFragment = new ModelDocumentFragment( model ); expect( modelToString( modelFragment ) ) @@ -681,7 +681,7 @@ describe( 'advanced-converters', () => { } } - data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, { batch } ) ); + data.output.appendChildren( conversionApi.convertChildren( data.input, consumable, batch ) ); } }, { priority: 'lowest' } ); @@ -705,7 +705,7 @@ describe( 'advanced-converters', () => { viewDispatcher.on( 'element:strong', ( evt, data, consumable, conversionApi ) => { if ( consumable.consume( data.input, { name: true } ) ) { if ( !data.output ) { - data.output = conversionApi.convertChildren( data.input, consumable, { batch } ); + data.output = conversionApi.convertChildren( data.input, consumable, batch ); } for ( const child of data.output ) { @@ -756,7 +756,7 @@ describe( 'advanced-converters', () => { ] ) ] ); - const modelElement = viewDispatcher.convert( viewElement, { batch } ); + const modelElement = viewDispatcher.convert( viewElement, batch ); expect( modelToString( modelElement ) ).to.equal( '
foobar
' + diff --git a/tests/conversion/buildviewconverter.js b/tests/conversion/buildviewconverter.js index 9bf2a6065..a4b90d0dd 100644 --- a/tests/conversion/buildviewconverter.js +++ b/tests/conversion/buildviewconverter.js @@ -72,7 +72,7 @@ describe( 'View converter builder', () => { batch = modelDocument.batch(); // `additionalData` parameter for `.convert` calls. - additionalData = { context: [ '$root' ], batch }; + additionalData = { context: [ '$root' ] }; schema = new ModelSchema(); @@ -100,7 +100,7 @@ describe( 'View converter builder', () => { it( 'should convert from view element to model element', () => { buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' ); - const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), additionalData ); + const conversionResult = dispatcher.convert( new ViewContainerElement( 'p', null, new ViewText( 'foo' ) ), batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -110,7 +110,7 @@ describe( 'View converter builder', () => { .fromElement( 'img' ) .toElement( viewElement => new ModelElement( 'image', { src: viewElement.getAttribute( 'src' ) } ) ); - const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), additionalData ); + const conversionResult = dispatcher.convert( new ViewContainerElement( 'img', { src: 'foo.jpg' } ), batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); } ); @@ -119,7 +119,7 @@ describe( 'View converter builder', () => { buildViewConverter().for( dispatcher ).fromElement( 'strong' ).toAttribute( 'bold', true ); const conversionResult = dispatcher.convert( - new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), additionalData + new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ), batch, additionalData ); // Have to check root because result is a ModelText. @@ -132,7 +132,7 @@ describe( 'View converter builder', () => { .toAttribute( viewElement => ( { key: 'linkHref', value: viewElement.getAttribute( 'href' ) } ) ); const conversionResult = dispatcher.convert( - new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), additionalData + new ViewAttributeElement( 'a', { href: 'foo.html' }, new ViewText( 'foo' ) ), batch, additionalData ); // Have to check root because result is a ModelText. @@ -147,7 +147,7 @@ describe( 'View converter builder', () => { .toAttribute( viewElement => ( { key: 'class', value: viewElement.getAttribute( 'class' ) } ) ); const conversionResult = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); @@ -169,7 +169,7 @@ describe( 'View converter builder', () => { new ViewContainerElement( 'p', { 'data-type': 'foo' }, new ViewText( 'xyz' ) ) ] ); - const conversionResult = dispatcher.convert( viewStructure, additionalData ); + const conversionResult = dispatcher.convert( viewStructure, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' + @@ -195,7 +195,7 @@ describe( 'View converter builder', () => { new ViewContainerElement( 'span', { style: 'font-weight:bold; font-size:20px' }, new ViewText( 'ddd' ) ) ] ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '<$text bold="true">aaabbbcccddd' ); } ); @@ -212,7 +212,7 @@ describe( 'View converter builder', () => { new ViewText( 'r' ) ] ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); const markerSearch = conversionResult.markers.get( 'search' ); @@ -234,7 +234,7 @@ describe( 'View converter builder', () => { new ViewText( 'r' ) ] ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); const markerSearch = conversionResult.markers.get( 'search' ); @@ -260,7 +260,7 @@ describe( 'View converter builder', () => { new ViewText( 'r' ) ] ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); const marker1 = conversionResult.markers.get( 'marker1' ); const marker2 = conversionResult.markers.get( 'marker2' ); @@ -277,7 +277,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'span' ); - const result = dispatcher.convert( element, additionalData ); + const result = dispatcher.convert( element, batch, additionalData ); expect( result ).to.be.instanceof( ModelDocumentFragment ); expect( result.childCount ).to.equal( 0 ); @@ -289,7 +289,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'marker', { class: 'search' } ); expect( () => { - dispatcher.convert( element, additionalData ); + dispatcher.convert( element, batch, additionalData ); } ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ ); } ); @@ -301,7 +301,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } ); expect( () => { - dispatcher.convert( element, additionalData ); + dispatcher.convert( element, batch, additionalData ); } ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ ); } ); @@ -313,7 +313,7 @@ describe( 'View converter builder', () => { const element = new ViewAttributeElement( 'marker', { 'data-name': 'search' } ); expect( () => { - dispatcher.convert( element, additionalData ); + dispatcher.convert( element, batch, additionalData ); } ).to.throw( CKEditorError, /^build-view-converter-invalid-marker/ ); } ); @@ -330,7 +330,7 @@ describe( 'View converter builder', () => { // Not quite megatron. result = dispatcher.convert( - new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), additionalData + new ViewContainerElement( 'span', { class: 'megatron' }, new ViewText( 'foo' ) ), batch, additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -338,6 +338,7 @@ describe( 'View converter builder', () => { // Almost a megatron. Missing a head. result = dispatcher.convert( new ViewContainerElement( 'span', { class: 'megatron', body: 'megatron', legs: 'megatron' }, new ViewText( 'foo' ) ), + batch, additionalData ); @@ -350,6 +351,7 @@ describe( 'View converter builder', () => { { class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' }, new ViewText( 'foo' ) ), + batch, additionalData ); @@ -362,6 +364,7 @@ describe( 'View converter builder', () => { { class: 'megatron', body: 'megatron', legs: 'megatron', head: 'megatron' }, new ViewText( 'foo' ) ), + batch, additionalData ); @@ -382,7 +385,7 @@ describe( 'View converter builder', () => { new ViewText( 'foo' ) ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -392,7 +395,7 @@ describe( 'View converter builder', () => { const viewElement = new ViewAttributeElement( 'strong', null, new ViewText( 'foo' ) ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( conversionResult.is( 'documentFragment' ) ).to.be.true; } ); @@ -404,7 +407,7 @@ describe( 'View converter builder', () => { buildViewConverter().for( dispatcher ).fromElement( 'p' ).toElement( 'paragraph' ); const conversionResult = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData ); // Element converter was fired first even though attribute converter was added first. @@ -420,7 +423,7 @@ describe( 'View converter builder', () => { let result; result = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -430,7 +433,7 @@ describe( 'View converter builder', () => { .toElement( 'customP' ); result = dispatcher.convert( - new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), additionalData + new ViewContainerElement( 'p', { class: 'myClass' }, new ViewText( 'foo' ) ), batch, additionalData ); expect( modelToString( result ) ).to.equal( 'foo' ); @@ -451,7 +454,7 @@ describe( 'View converter builder', () => { .toAttribute( 'size', 'small' ); const viewElement = new ViewContainerElement( 'p', { class: 'decorated small' }, new ViewText( 'foo' ) ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); // P element and it's children got converted by the converter (1) and the converter (1) got fired // because P name was not consumed in converter (2). Converter (3) could consume class="small" because @@ -474,7 +477,7 @@ describe( 'View converter builder', () => { new ViewContainerElement( 'abcd', null, new ViewText( 'foo' ) ) ] ); - const conversionResult = dispatcher.convert( viewStructure, additionalData ); + const conversionResult = dispatcher.convert( viewStructure, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '
foo
' ); } ); @@ -493,7 +496,7 @@ describe( 'View converter builder', () => { ) ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -512,7 +515,7 @@ describe( 'View converter builder', () => { ) ); - const conversionResult = dispatcher.convert( viewElement, additionalData ); + const conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( 'foo' ); } ); @@ -526,11 +529,11 @@ describe( 'View converter builder', () => { } ); const viewElement = new ViewContainerElement( 'p' ); - let conversionResult = dispatcher.convert( viewElement, additionalData ); + let conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); viewElement.setAttribute( 'stop', true ); - conversionResult = dispatcher.convert( viewElement, additionalData ); + conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 0 ); @@ -548,11 +551,11 @@ describe( 'View converter builder', () => { } ); const viewElement = new ViewContainerElement( 'p', { 'data-type': 'foo' } ); - let conversionResult = dispatcher.convert( viewElement, additionalData ); + let conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); viewElement.setAttribute( 'data-type', 'stop' ); - conversionResult = dispatcher.convert( viewElement, additionalData ); + conversionResult = dispatcher.convert( viewElement, batch, additionalData ); expect( modelToString( conversionResult ) ).to.equal( '' ); } ); } ); diff --git a/tests/conversion/view-to-model-converters.js b/tests/conversion/view-to-model-converters.js index b4f19525c..550946ee8 100644 --- a/tests/conversion/view-to-model-converters.js +++ b/tests/conversion/view-to-model-converters.js @@ -26,7 +26,7 @@ describe( 'view-to-model-converters', () => { schema.registerItem( 'paragraph', '$block' ); schema.allow( { name: '$text', inside: '$root' } ); batch = modelDocument.batch(); - additionalData = { context: [ '$root' ], batch }; + additionalData = { context: [ '$root' ] }; dispatcher = new ViewConversionDispatcher( { schema } ); } ); @@ -36,7 +36,7 @@ describe( 'view-to-model-converters', () => { dispatcher.on( 'text', convertText() ); - const conversionResult = dispatcher.convert( viewText, additionalData ); + const conversionResult = dispatcher.convert( viewText, batch, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -55,7 +55,7 @@ describe( 'view-to-model-converters', () => { } } ); - const conversionResult = dispatcher.convert( viewText, additionalData ); + const conversionResult = dispatcher.convert( viewText, batch, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -68,12 +68,12 @@ describe( 'view-to-model-converters', () => { const viewText = new ViewText( 'foobar' ); dispatcher.on( 'text', convertText() ); - let conversionResult = dispatcher.convert( viewText, additionalData ); + let conversionResult = dispatcher.convert( viewText, batch, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 0 ); - conversionResult = dispatcher.convert( viewText, { context: [ '$block' ], batch } ); + conversionResult = dispatcher.convert( viewText, batch, { context: [ '$block' ] } ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 1 ); @@ -86,7 +86,7 @@ describe( 'view-to-model-converters', () => { dispatcher.on( 'text', convertText() ); - const conversionResult = dispatcher.convert( viewText, additionalData ); + const conversionResult = dispatcher.convert( viewText, batch, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -107,7 +107,7 @@ describe( 'view-to-model-converters', () => { dispatcher.on( 'element', convertToModelFragment() ); dispatcher.on( 'documentFragment', convertToModelFragment() ); - const conversionResult = dispatcher.convert( viewFragment, additionalData ); + const conversionResult = dispatcher.convert( viewFragment, batch, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.maxOffset ).to.equal( 6 ); @@ -132,7 +132,7 @@ describe( 'view-to-model-converters', () => { } } ); - const conversionResult = dispatcher.convert( viewP, additionalData ); + const conversionResult = dispatcher.convert( viewP, batch, additionalData ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement ); diff --git a/tests/conversion/viewconversiondispatcher.js b/tests/conversion/viewconversiondispatcher.js index 1ef8728fc..7a5d8f06a 100644 --- a/tests/conversion/viewconversiondispatcher.js +++ b/tests/conversion/viewconversiondispatcher.js @@ -51,7 +51,7 @@ describe( 'ViewConversionDispatcher', () => { sinon.spy( dispatcher, 'fire' ); const viewP = new ViewContainerElement( 'p' ); - dispatcher.convert( viewP, { batch } ); + dispatcher.convert( viewP, batch ); expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true; } ); @@ -65,9 +65,9 @@ describe( 'ViewConversionDispatcher', () => { sinon.spy( dispatcher, 'fire' ); - dispatcher.convert( viewText, { batch } ); - dispatcher.convert( viewElement, { batch } ); - dispatcher.convert( viewFragment, { batch } ); + dispatcher.convert( viewText, batch ); + dispatcher.convert( viewElement, batch ); + dispatcher.convert( viewFragment, batch ); expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true; expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true; @@ -99,7 +99,7 @@ describe( 'ViewConversionDispatcher', () => { } ); // Use `additionalData` parameter to check if it was passed to the event. - const conversionResult = dispatcher.convert( viewText, { foo: 'bar', batch } ); + const conversionResult = dispatcher.convert( viewText, batch, { foo: 'bar' } ); // Check conversion result. // Result should be wrapped in document fragment. @@ -134,7 +134,7 @@ describe( 'ViewConversionDispatcher', () => { } ); // Use `additionalData` parameter to check if it was passed to the event. - const conversionResult = dispatcher.convert( viewElement, { foo: 'bar', batch } ); + const conversionResult = dispatcher.convert( viewElement, batch, { foo: 'bar' } ); // Check conversion result. // Result should be wrapped in document fragment. @@ -168,7 +168,7 @@ describe( 'ViewConversionDispatcher', () => { } ); // Use `additionalData` parameter to check if it was passed to the event. - const conversionResult = dispatcher.convert( viewFragment, { foo: 'bar', batch } ); + const conversionResult = dispatcher.convert( viewFragment, batch, { foo: 'bar' } ); // Check conversion result. expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); @@ -191,7 +191,7 @@ describe( 'ViewConversionDispatcher', () => { ] ); } ); - const conversionResult = dispatcher.convert( viewFragment, { batch } ); + const conversionResult = dispatcher.convert( viewFragment, batch ); expect( conversionResult.markers.size ).to.equal( 2 ); expect( stringify( conversionResult, conversionResult.markers.get( 'marker1' ) ) ).to.deep.equal( 'fo[ob]ar' ); @@ -268,10 +268,10 @@ describe( 'ViewConversionDispatcher', () => { expect( conversionApi.convertItem( viewP, consumableMock, data ) ).to.equal( modelP ); expect( conversionApi.convertItem( viewText, consumableMock, data ) ).to.equal( modelText ); - expect( data.batch ).to.equal( batch ); + expect( conversionApi.batch ).to.equal( batch ); } ); - dispatcher.convert( new ViewDocumentFragment(), { foo: 'bar', batch } ); + dispatcher.convert( new ViewDocumentFragment(), batch, { foo: 'bar' } ); expect( spy.calledOnce ).to.be.true; expect( spyP.calledOnce ).to.be.true; @@ -288,7 +288,7 @@ describe( 'ViewConversionDispatcher', () => { expect( conversionApi.convertItem( viewNull ) ).to.equal( null ); } ); - dispatcher.convert( new ViewDocumentFragment(), { batch } ); + dispatcher.convert( new ViewDocumentFragment(), batch ); expect( spy.calledOnce ).to.be.true; expect( spyNull.calledOnce ).to.be.true; @@ -306,7 +306,7 @@ describe( 'ViewConversionDispatcher', () => { expect( conversionApi.convertItem( viewArray ) ).to.equal( null ); } ); - dispatcher.convert( new ViewDocumentFragment(), { batch } ); + dispatcher.convert( new ViewDocumentFragment(), batch ); expect( spy.calledOnce ).to.be.true; expect( spyArray.calledOnce ).to.be.true; @@ -332,7 +332,7 @@ describe( 'ViewConversionDispatcher', () => { expect( result.getChild( 1 ) ).to.equal( modelText ); } ); - dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), { foo: 'bar', batch } ); + dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), batch, { foo: 'bar' } ); expect( spy.calledOnce ).to.be.true; expect( spyP.calledOnce ).to.be.true; @@ -353,7 +353,7 @@ describe( 'ViewConversionDispatcher', () => { expect( result.getChild( 1 ) ).to.equal( modelText ); } ); - dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), { foo: 'bar', batch } ); + dispatcher.convert( new ViewDocumentFragment( [ viewArray, viewP, viewDiv, viewText, viewNull ] ), batch, { foo: 'bar' } ); expect( spy.calledOnce ).to.be.true; expect( spyNull.calledOnce ).to.be.true; From f183da5a9fabdb5a7f540aa486cb8f66ba174308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Tue, 28 Nov 2017 16:03:09 +0100 Subject: [PATCH 44/44] Simplified some code. --- src/conversion/viewconversiondispatcher.js | 6 +----- src/model/documentselection.js | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/conversion/viewconversiondispatcher.js b/src/conversion/viewconversiondispatcher.js index 6b1f8a101..36ee9de70 100644 --- a/src/conversion/viewconversiondispatcher.js +++ b/src/conversion/viewconversiondispatcher.js @@ -218,12 +218,8 @@ export default class ViewConversionDispatcher { for ( const viewChild of Array.from( input.getChildren() ) ) { const modelChild = this._convertItem( viewChild, consumable, additionalData ); - if ( modelChild instanceof ModelNode ) { + if ( modelChild instanceof ModelNode || modelChild instanceof ModelDocumentFragment ) { batch.append( modelChild, documentFragment ); - } else if ( modelChild instanceof ModelDocumentFragment ) { - for ( const child of Array.from( modelChild ) ) { - batch.append( child, documentFragment ); - } } } diff --git a/src/model/documentselection.js b/src/model/documentselection.js index 2b44043da..f213430cd 100644 --- a/src/model/documentselection.js +++ b/src/model/documentselection.js @@ -723,7 +723,7 @@ function clearAttributesStoredInElement( changes, batch, document ) { // `changes.range` is not set in case of rename, root and marker operations. // None of them may lead to the element becoming non-empty. - if ( !changeParent || changeParent.is( 'documentFragment' ) || changeParent.isEmpty ) { + if ( !changeParent || changeParent.isEmpty ) { return; }