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

Commit

Permalink
Merge pull request #1606 from ckeditor/t/1604
Browse files Browse the repository at this point in the history
Feature: Added an additional event in markers conversion. Improved how MarkerOperation is transformed during undo. Closes #1604.
  • Loading branch information
Piotr Jasiun authored Dec 21, 2018
2 parents de4eb63 + 6d56060 commit da5a390
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 47 deletions.
4 changes: 2 additions & 2 deletions src/conversion/downcast-converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ export function wrap( elementCreator ) {
*/
export function highlightText( highlightDescriptor ) {
return ( evt, data, conversionApi ) => {
if ( data.markerRange.isCollapsed ) {
if ( !data.item ) {
return;
}

Expand Down Expand Up @@ -921,7 +921,7 @@ export function highlightText( highlightDescriptor ) {
*/
export function highlightElement( highlightDescriptor ) {
return ( evt, data, conversionApi ) => {
if ( data.markerRange.isCollapsed ) {
if ( !data.item ) {
return;
}

Expand Down
22 changes: 14 additions & 8 deletions src/conversion/downcastdispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,22 +321,28 @@ export default class DowncastDispatcher {
// In markers' case, event name == consumable name.
const eventName = 'addMarker:' + markerName;

// When range is collapsed - fire single event with collapsed range in consumable.
if ( markerRange.isCollapsed ) {
const consumable = new Consumable();
consumable.add( markerRange, eventName );
//
// First, fire an event for the whole marker.
//
const consumable = new Consumable();
consumable.add( markerRange, eventName );

this.conversionApi.consumable = consumable;
this.conversionApi.consumable = consumable;

this.fire( eventName, { markerName, markerRange }, this.conversionApi );
this.fire( eventName, { markerName, markerRange }, this.conversionApi );

//
// Do not fire events for each item inside the range if the range got consumed.
//
if ( !consumable.test( markerRange, eventName ) ) {
return;
}

// Create consumable for each item in range.
//
// Then, fire an event for each item inside the marker range.
//
this.conversionApi.consumable = this._createConsumableForRange( markerRange, eventName );

// Create separate event for each node in the range.
for ( const item of markerRange.getItems() ) {
// Do not fire event for already consumed items.
if ( !this.conversionApi.consumable.test( item, eventName ) ) {
Expand Down
2 changes: 1 addition & 1 deletion src/model/markercollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export default class MarkerCollection {
* Fired whenever marker is added, updated or removed from `MarkerCollection`.
*
* @event update
* @param {module:engine/model/markercollection~Marker} Updated Marker.
* @param {module:engine/model/markercollection~Marker} marker Updated Marker.
* @param {module:engine/model/range~Range|null} oldRange Marker range before the update. When is not defined it
* means that marker is just added.
* @param {module:engine/model/range~Range|null} newRange Marker range after update. When is not defined it
Expand Down
72 changes: 70 additions & 2 deletions src/model/operation/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,48 @@ class ContextFactory {

break;
}

case MarkerOperation: {
const markerRange = opA.newRange;

if ( !markerRange ) {
return;
}

switch ( opB.constructor ) {
case MoveOperation: {
const movedRange = Range._createFromPositionAndShift( opB.sourcePosition, opB.howMany );

const affectedLeft = movedRange.containsPosition( markerRange.start ) ||
movedRange.start.isEqual( markerRange.start );

const affectedRight = movedRange.containsPosition( markerRange.end ) ||
movedRange.end.isEqual( markerRange.end );

if ( ( affectedLeft || affectedRight ) && !movedRange.containsRange( markerRange ) ) {
this._setRelation( opA, opB, {
side: affectedLeft ? 'left' : 'right',
offset: affectedLeft ? markerRange.start.offset : markerRange.end.offset
} );
}

break;
}

case MergeOperation: {
const wasInLeftElement = markerRange.start.isEqual( opB.targetPosition );
const wasInRightElement = markerRange.end.isEqual( opB.sourcePosition );

if ( wasInLeftElement || wasInRightElement ) {
this._setRelation( opA, opB, { wasInLeftElement, wasInRightElement } );
}

break;
}
}

break;
}
}
}

Expand Down Expand Up @@ -1068,24 +1110,49 @@ setTransformation( MarkerOperation, MergeOperation, ( a, b ) => {
return [ a ];
} );

setTransformation( MarkerOperation, MoveOperation, ( a, b ) => {
setTransformation( MarkerOperation, MoveOperation, ( a, b, context ) => {
if ( a.oldRange ) {
a.oldRange = Range._createFromRanges( a.oldRange._getTransformedByMoveOperation( b ) );
}

if ( a.newRange ) {
if ( context.abRelation ) {
if ( context.abRelation.side == 'left' && b.targetPosition.isEqual( a.newRange.start ) ) {
a.newRange.start.offset = context.abRelation.offset;
a.newRange.end.offset += b.howMany;

return [ a ];
} else if ( context.abRelation.side == 'right' && b.targetPosition.isEqual( a.newRange.end ) ) {
a.newRange.end.offset = context.abRelation.offset;

return [ a ];
}
}

a.newRange = Range._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
}

return [ a ];
} );

setTransformation( MarkerOperation, SplitOperation, ( a, b ) => {
setTransformation( MarkerOperation, SplitOperation, ( a, b, context ) => {
if ( a.oldRange ) {
a.oldRange = a.oldRange._getTransformedBySplitOperation( b );
}

if ( a.newRange ) {
if ( context.abRelation ) {
if ( a.newRange.start.isEqual( b.splitPosition ) && !context.abRelation.wasInLeftElement ) {
a.newRange.start = Position._createAt( b.moveTargetPosition );
}

if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasInRightElement ) {
a.newRange.end = Position._createAt( b.moveTargetPosition );
}

return [ a ];
}

a.newRange = a.newRange._getTransformedBySplitOperation( b );
}

Expand Down Expand Up @@ -1719,6 +1786,7 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
targetPositionPath.push( 0 );

const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, targetPositionPath );
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( gyMoveSource, gyMoveTarget, 1 );
const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );

results.push( gyMove );
Expand Down
15 changes: 15 additions & 0 deletions src/model/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,21 @@ export default class Range {
* @returns {module:engine/model/range~Range}
*/
_getTransformedByMergeOperation( operation ) {
// Special case when the marker is set on "the closing tag" of an element. Marker can be set like that during
// transformations, especially when a content of a few block elements were removed. For example:
//
// {} is the transformed range, [] is the removed range.
// <p>F[o{o</p><p>B}ar</p><p>Xy]z</p>
//
// <p>Fo{o</p><p>B}ar</p><p>z</p>
// <p>F{</p><p>B}ar</p><p>z</p>
// <p>F{</p>}<p>z</p>
// <p>F{}z</p>
//
if ( this.start.isEqual( operation.targetPosition ) && this.end.isEqual( operation.deletionPosition ) ) {
return new Range( this.start );
}

let start = this.start._getTransformedByMergeOperation( operation );
let end = this.end._getTransformedByMergeOperation( operation );

Expand Down
77 changes: 65 additions & 12 deletions src/model/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,14 @@ export default class Writer {

const position = Position._createAt( itemOrPosition, offset );

// Do not move anything if the move target is same as moved range start.
if ( position.isEqual( range.start ) ) {
return;
}

// If part of the marker is removed, create additional marker operation for undo purposes.
this._addOperationForAffectedMarkers( 'move', range );

if ( !isSameTree( range.root, position.root ) ) {
/**
* Range is going to be moved within not the same document. Please use
Expand All @@ -491,17 +499,15 @@ export default class Writer {
remove( itemOrRange ) {
this._assertWriterUsedCorrectly();

if ( itemOrRange instanceof Range ) {
// The array is reversed, so the ranges to remove are in correct order and do not have to be updated.
const ranges = itemOrRange.getMinimalFlatRanges().reverse();
const rangeToRemove = itemOrRange instanceof Range ? itemOrRange : Range._createOn( itemOrRange );

for ( const flat of ranges ) {
applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model );
}
} else {
const howMany = itemOrRange.is( 'text' ) ? itemOrRange.offsetSize : 1;
// If part of the marker is removed, create additional marker operation for undo purposes.
this._addOperationForAffectedMarkers( 'move', rangeToRemove );

applyRemoveOperation( Position._createBefore( itemOrRange ), howMany, this.batch, this.model );
const ranges = rangeToRemove.getMinimalFlatRanges().reverse();

for ( const flat of ranges ) {
applyRemoveOperation( flat.start, flat.end.offset - flat.start.offset, this.batch, this.model );
}
}

Expand All @@ -519,6 +525,9 @@ export default class Writer {
const nodeBefore = position.nodeBefore;
const nodeAfter = position.nodeAfter;

// If part of the marker is removed, create additional marker operation for undo purposes.
this._addOperationForAffectedMarkers( 'merge', position );

if ( !( nodeBefore instanceof Element ) ) {
/**
* Node before merge position must be an element.
Expand Down Expand Up @@ -888,12 +897,12 @@ export default class Writer {

if ( !options || typeof options.usingOperation != 'boolean' ) {
/**
* The `options.usingOperations` parameter is required when adding a new marker.
* The `options.usingOperation` parameter is required when adding a new marker.
*
* @error writer-addMarker-no-usingOperations
* @error writer-addMarker-no-usingOperation
*/
throw new CKEditorError(
'writer-addMarker-no-usingOperations: The options.usingOperations parameter is required when adding a new marker.'
'writer-addMarker-no-usingOperation: The options.usingOperation parameter is required when adding a new marker.'
);
}

Expand Down Expand Up @@ -1294,6 +1303,50 @@ export default class Writer {
throw new CKEditorError( 'writer-incorrect-use: Trying to use a writer outside the change() block.' );
}
}

/**
* For given action `type` and `positionOrRange` where the action happens, this function finds all affected markers
* and applies a marker operation with the new marker range equal to the current range. Thanks to this, the marker range
* can be later correctly processed during undo.
*
* @private
* @param {'move'|'merge'} type Writer action type.
* @param {module:engine/model/position~Position|module:engine/model/range~Range} positionOrRange Position or range
* where the writer action happens.
*/
_addOperationForAffectedMarkers( type, positionOrRange ) {
for ( const marker of this.model.markers ) {
if ( !marker.managedUsingOperations ) {
continue;
}

const markerRange = marker.getRange();
let isAffected = false;

if ( type == 'move' ) {
const intersecting =
positionOrRange.containsPosition( markerRange.start ) ||
positionOrRange.start.isEqual( markerRange.start ) ||
positionOrRange.containsPosition( markerRange.end ) ||
positionOrRange.end.isEqual( markerRange.end );

isAffected = intersecting && !positionOrRange.containsRange( markerRange );
} else {
// if type == 'merge'.
const elementBefore = positionOrRange.nodeBefore;
const elementAfter = positionOrRange.nodeAfter;

const affectedOnLeft = markerRange.start.parent == elementBefore && markerRange.start.isAtEnd;
const affectedOnRight = markerRange.end.parent == elementAfter && markerRange.end.offset == 0;

isAffected = affectedOnLeft || affectedOnRight;
}

if ( isAffected ) {
this.updateMarker( marker.name, { range: markerRange } );
}
}
}
}

// Sets given attribute to each node in given range. When attribute value is null then attribute will be removed.
Expand Down
Loading

0 comments on commit da5a390

Please sign in to comment.