Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge branch 'master' into t/ckeditor5-typing/101
Browse files Browse the repository at this point in the history
  • Loading branch information
Reinmar committed Aug 22, 2017
2 parents 4aaaf7b + ed1b7e7 commit 9515717
Show file tree
Hide file tree
Showing 12 changed files with 424 additions and 125 deletions.
84 changes: 51 additions & 33 deletions src/model/delta/basic-transformations.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,16 @@ addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
return defaultTransform( a, b, context );
}

const undoMode = context.aWasUndone || context.bWasUndone;
const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );

const deltas = defaultTransform( a, b, context );

// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
if ( undoMode || !( b._cloneOperation instanceof InsertOperation ) ) {
return deltas;
}

for ( const operation of a.operations ) {
// If a node that has been split has it's attribute updated, we should also update attribute of
// the node created during splitting.
Expand Down Expand Up @@ -292,21 +298,25 @@ addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {

a = a.clone();

const undoMode = context.aWasUndone || context.bWasUndone;
const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );

if ( a._cloneOperation instanceof InsertOperation ) {
// If element to split had it's attribute changed, we have to reflect this change in an element
// that is in SplitDelta's InsertOperation.
for ( const operation of b.operations ) {
if ( operation.range.containsPosition( splitPosition ) || operation.range.start.isEqual( splitPosition ) ) {
if ( operation.newValue !== null ) {
a._cloneOperation.nodes.getNode( 0 ).setAttribute( operation.key, operation.newValue );
} else {
a._cloneOperation.nodes.getNode( 0 ).removeAttribute( operation.key );
}

break;
// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
if ( undoMode || !( a._cloneOperation instanceof InsertOperation ) ) {
return [ a ];
}

// If element to split had it's attribute changed, we have to reflect this change in an element
// that is in SplitDelta's InsertOperation.
for ( const operation of b.operations ) {
if ( operation.range.containsPosition( splitPosition ) || operation.range.start.isEqual( splitPosition ) ) {
if ( operation.newValue !== null ) {
a._cloneOperation.nodes.getNode( 0 ).setAttribute( operation.key, operation.newValue );
} else {
a._cloneOperation.nodes.getNode( 0 ).removeAttribute( operation.key );
}

break;
}
}

Expand Down Expand Up @@ -383,18 +393,16 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
// Add special case for RenameDelta x SplitDelta transformation.
addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
const undoMode = context.aWasUndone || context.bWasUndone;
const deltas = defaultTransform( a, b, context );

// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
let insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;

if ( insertPosition ) {
insertPosition = insertPosition.getShiftedBy( -1 );
// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
if ( undoMode || !( b._cloneOperation instanceof InsertOperation ) ) {
return deltas;
}

const deltas = defaultTransform( a, b, context );
const insertPosition = b._cloneOperation.position.getShiftedBy( -1 );

if ( insertPosition && !undoMode && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
if ( insertPosition && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
// If a node that has been split has it's name changed, we should also change name of
// the node created during splitting.
const additionalRenameDelta = a.clone();
Expand All @@ -408,31 +416,27 @@ addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {

// Add special case for SplitDelta x RenameDelta transformation.
addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
const undoMode = context.aWasUndone || context.bWasUndone;
a = a.clone();

// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
let insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;
const undoMode = context.aWasUndone || context.bWasUndone;

if ( insertPosition ) {
insertPosition = insertPosition.getShiftedBy( -1 );
// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
if ( undoMode || !( a._cloneOperation instanceof InsertOperation ) ) {
return [ a ];
}

const insertPosition = a._cloneOperation.position.getShiftedBy( -1 );

// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
if ( insertPosition && !undoMode && b.operations[ 0 ].position.isEqual( insertPosition ) ) {
const additionalRenameDelta = b.clone();
additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );

// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.
// However, such delta is only created when `MergeDelta` is reversed.
// So if this is not undo mode, it means that `SplitDelta` has `InsertOperation`.
additionalRenameDelta.operations[ 0 ].oldName = a._cloneOperation.nodes.getNode( 0 ).name;

return [ a.clone(), additionalRenameDelta ];
return [ a, additionalRenameDelta ];
}

return [ a.clone() ];
return [ a ];
} );

// Add special case for RemoveDelta x SplitDelta transformation.
Expand All @@ -446,6 +450,13 @@ addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {
return defaultTransform( a, b, context );
}

const undoMode = context.aWasUndone || context.bWasUndone;

// Special case applies only if undo is not a context.
if ( undoMode ) {
return deltas;
}

// In case if `defaultTransform` returned more than one delta.
for ( const delta of deltas ) {
// "No delta" may be returned in some cases.
Expand All @@ -464,6 +475,13 @@ addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {

// Add special case for SplitDelta x RemoveDelta transformation.
addTransformationCase( SplitDelta, RemoveDelta, ( a, b, context ) => {
const undoMode = context.aWasUndone || context.bWasUndone;

// Special case applies only if undo is not a context.
if ( undoMode ) {
return defaultTransform( a, b, context );
}

// This case is very trickily solved.
// Instead of fixing `a` delta, we change `b` delta for a while and fire default transformation with fixed `b` delta.
// Thanks to that fixing `a` delta will be differently (correctly) transformed.
Expand Down
16 changes: 9 additions & 7 deletions src/model/documentselection.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default class DocumentSelection extends Selection {

// Whenever element which had selection's attributes stored in it stops being empty,
// the attributes need to be removed.
clearAttributesStoredInElement( changes, batch );
clearAttributesStoredInElement( changes, batch, this._document );
} );
}

Expand Down Expand Up @@ -312,7 +312,7 @@ export default class DocumentSelection extends Selection {

const liveRange = LiveRange.createFromRange( range );

this.listenTo( liveRange, 'change', ( evt, oldRange, data ) => {
this.listenTo( liveRange, 'change:range', ( evt, oldRange, data ) => {
// If `LiveRange` is in whole moved to the graveyard, fix that range.
if ( liveRange.root == this._document.graveyard ) {
const sourceStart = data.sourcePosition;
Expand Down Expand Up @@ -691,7 +691,7 @@ function getAttrsIfCharacter( node ) {
}

// Removes selection attributes from element which is not empty anymore.
function clearAttributesStoredInElement( changes, batch ) {
function clearAttributesStoredInElement( changes, batch, document ) {
// Batch may not be passed to the document#change event in some tests.
// See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
// Ignore also transparent batches because they are... transparent.
Expand All @@ -707,9 +707,11 @@ function clearAttributesStoredInElement( changes, batch ) {
return;
}

const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
document.enqueueChanges( () => {
const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );

for ( const key of storedAttributes ) {
batch.removeAttribute( changeParent, key );
}
for ( const key of storedAttributes ) {
batch.removeAttribute( changeParent, key );
}
} );
}
40 changes: 35 additions & 5 deletions src/model/liverange.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ export default class LiveRange extends Range {
*/

/**
* Fired when `LiveRange` instance is changed due to changes in the {@link module:engine/model/document~Document document}.
* Fired when `LiveRange` instance boundaries have changed due to changes in the
* {@link module:engine/model/document~Document document}.
*
* @event change
* @event change:range
* @param {module:engine/model/range~Range} oldRange Range with start and end position equal to start and end position of this live
* range before it got changed.
* @param {Object} data Object with additional information about the change. Those parameters are passed from
Expand All @@ -89,6 +90,21 @@ export default class LiveRange extends Range {
* @param {module:engine/model/range~Range} data.range Range containing the result of applied change.
* @param {module:engine/model/position~Position} data.sourcePosition Source position for move, remove and reinsert change types.
*/

/**
* Fired when `LiveRange` instance boundaries have not changed after a change in {@link module:engine/model/document~Document document}
* but the change took place inside the range, effectively changing its content.
*
* @event change:content
* @param {module:engine/model/range~Range} range Range with start and end position equal to start and end position of
* change range.
* @param {Object} data Object with additional information about the change. Those parameters are passed from
* {@link module:engine/model/document~Document#event:change document change event}.
* @param {String} data.type Change type.
* @param {module:engine/model/batch~Batch} data.batch Batch which changed the live range.
* @param {module:engine/model/range~Range} data.range Range containing the result of applied change.
* @param {module:engine/model/position~Position} data.sourcePosition Source position for move, remove and reinsert change types.
*/
}

/**
Expand Down Expand Up @@ -152,14 +168,28 @@ function transform( changeType, deltaType, batch, targetRange, sourcePosition )

const updated = Range.createFromRanges( result );

// If anything changed, update the range and fire an event.
if ( !updated.isEqual( this ) ) {
const boundariesChanged = !updated.isEqual( this );

const rangeExpanded = this.containsPosition( targetPosition );
const rangeShrunk = sourcePosition && ( this.containsPosition( sourcePosition ) || this.start.isEqual( sourcePosition ) );
const contentChanged = rangeExpanded || rangeShrunk;

if ( boundariesChanged ) {
// If range boundaries have changed, fire `change:range` event.
const oldRange = Range.createFromRange( this );

this.start = updated.start;
this.end = updated.end;

this.fire( 'change', oldRange, {
this.fire( 'change:range', oldRange, {
type: changeType,
batch,
range: targetRange,
sourcePosition
} );
} else if ( contentChanged ) {
// If range boundaries have not changed, but there was change inside the range, fire `change:content` event.
this.fire( 'change:content', Range.createFromRange( this ), {
type: changeType,
batch,
range: targetRange,
Expand Down
4 changes: 3 additions & 1 deletion src/model/markercollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,9 @@ class Marker {
*/
this._liveRange = liveRange;

this._liveRange.delegate( 'change' ).to( this );
// Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
this._liveRange.delegate( 'change:range' ).to( this );
this._liveRange.delegate( 'change:content' ).to( this );
}

/**
Expand Down
8 changes: 4 additions & 4 deletions tests/model/delta/transform/_utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,14 @@ export function expectDelta( delta, expected ) {
export function expectOperation( op, params ) {
for ( const i in params ) {
if ( i == 'type' ) {
expect( op ).to.be.instanceof( params[ i ] );
expect( op, 'operation type' ).to.be.instanceof( params[ i ] );
}
else if ( i == 'nodes' ) {
expect( Array.from( op.nodes ) ).to.deep.equal( params[ i ] );
expect( Array.from( op.nodes ), 'nodes' ).to.deep.equal( params[ i ] );
} else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
expect( op[ i ].isEqual( params[ i ] ) ).to.be.true;
expect( op[ i ].isEqual( params[ i ] ), 'property ' + i ).to.be.true;
} else {
expect( op[ i ] ).to.equal( params[ i ] );
expect( op[ i ], 'property ' + 1 ).to.equal( params[ i ] );
}
}
}
Expand Down
51 changes: 51 additions & 0 deletions tests/model/delta/transform/attributedelta.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import Range from '../../../../src/model/range';

import AttributeDelta from '../../../../src/model/delta/attributedelta';
import Delta from '../../../../src/model/delta/delta';
import SplitDelta from '../../../../src/model/delta/splitdelta';
import AttributeOperation from '../../../../src/model/operation/attributeoperation';
import MoveOperation from '../../../../src/model/operation/moveoperation';
import ReinsertOperation from '../../../../src/model/operation/reinsertoperation';
import NoOperation from '../../../../src/model/operation/nooperation';

import {
Expand Down Expand Up @@ -257,6 +260,54 @@ describe( 'transform', () => {
} );
} );

it( 'change attribute of split element that reinserts from graveyard', () => {
const gy = doc.graveyard;
const splitDelta = new SplitDelta();

splitDelta.operations[ 0 ] = new ReinsertOperation(
new Position( gy, [ 1 ] ),
1,
new Position( root, [ 2 ] ),
baseVersion
);

splitDelta.operations[ 1 ] = new MoveOperation(
new Position( root, [ 1, 4 ] ),
4,
new Position( root, [ 2, 0 ] ),
baseVersion
);

const attributeDelta = new AttributeDelta();

const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 1, 0 ] ) );

attributeDelta.addOperation( new AttributeOperation( range, 'key', 'oldValue', 'newValue', baseVersion ) );

// The split delta was undone.
context.bWasUndone = true;

const transformed = transform( attributeDelta, splitDelta, context );

baseVersion = attributeDelta.operations.length;
// We expect only one delta. Split delta should not affect attribute delta transformation. It was undone.
expect( transformed.length ).to.equal( 1 );

expectDelta( transformed[ 0 ], {
type: AttributeDelta,
operations: [
{
type: AttributeOperation,
range,
key: 'key',
oldValue: 'oldValue',
newValue: 'newValue',
baseVersion
}
]
} );
} );

it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 3 ] ) );
const attrDelta = getAttributeDelta( range, 'foo', null, 'bar', 0 );
Expand Down
29 changes: 29 additions & 0 deletions tests/model/delta/transform/removedelta.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,35 @@ describe( 'transform', () => {
} );
} );

it( 'removed node has been split - undo context', () => {
const sourcePosition = new Position( root, [ 3, 3, 1 ] );
const removeDelta = getRemoveDelta( sourcePosition, 1, baseVersion );

const splitPosition = new Position( root, [ 3, 3, 1, 2 ] );
const nodeCopy = new Element( 'x' );
const splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );

context.bWasUndone = true;

const transformed = transform( removeDelta, splitDelta, context );

expect( transformed.length ).to.equal( 1 );

baseVersion = splitDelta.operations.length;

expectDelta( transformed[ 0 ], {
type: RemoveDelta,
operations: [
{
type: RemoveOperation,
sourcePosition,
howMany: 1,
baseVersion
}
]
} );
} );

it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
const noOpSplitDelta = new SplitDelta();
noOpSplitDelta.addOperation( new NoOperation( 0 ) );
Expand Down
Loading

0 comments on commit 9515717

Please sign in to comment.