ckeditor5-image-upload-base64
Version:
The development environment of CKEditor 5 – the best browser-based rich text editor.
204 lines (170 loc) • 6.97 kB
JavaScript
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module undo/basecommand
*/
import Command from '@ckeditor/ckeditor5-core/src/command';
import { transformSets } from '@ckeditor/ckeditor5-engine/src/model/operation/transform';
/**
* Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}.
*
* @protected
* @extends module:core/command~Command
*/
export default class BaseCommand extends Command {
constructor( editor ) {
super( editor );
/**
* Stack of items stored by the command. These are pairs of:
*
* * {@link module:engine/model/batch~Batch batch} saved by the command,
* * {@link module:engine/model/selection~Selection selection} state at the moment of saving the batch.
*
* @protected
* @member {Array} #_stack
*/
this._stack = [];
/**
* Stores all batches that were created by this command.
*
* @protected
* @member {WeakSet.<module:engine/model/batch~Batch>} #_createdBatches
*/
this._createdBatches = new WeakSet();
// Refresh state, so the command is inactive right after initialization.
this.refresh();
this.listenTo( editor.data, 'set', () => this.clearStack() );
}
/**
* @inheritDoc
*/
refresh() {
this.isEnabled = this._stack.length > 0;
}
/**
* Stores a batch in the command, together with the selection state of the {@link module:engine/model/document~Document document}
* created by the editor which this command is registered to.
*
* @param {module:engine/model/batch~Batch} batch The batch to add.
*/
addBatch( batch ) {
const docSelection = this.editor.model.document.selection;
const selection = {
ranges: docSelection.hasOwnRange ? Array.from( docSelection.getRanges() ) : [],
isBackward: docSelection.isBackward
};
this._stack.push( { batch, selection } );
this.refresh();
}
/**
* Removes all items from the stack.
*/
clearStack() {
this._stack = [];
this.refresh();
}
/**
* Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone.
*
* @protected
* @param {Array.<module:engine/model/range~Range>} ranges Ranges to be restored.
* @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward.
* @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations which has been applied
* since selection has been stored.
*/
_restoreSelection( ranges, isBackward, operations ) {
const model = this.editor.model;
const document = model.document;
// This will keep the transformed selection ranges.
const selectionRanges = [];
// Transform all ranges from the restored selection.
const transformedRangeGroups = ranges.map( range => range.getTransformedByOperations( operations ) );
const allRanges = transformedRangeGroups.flat();
for ( const rangeGroup of transformedRangeGroups ) {
// While transforming there could appear ranges that are contained by other ranges, we shall ignore them.
const transformed = rangeGroup.filter( range => !isRangeContainedByAnyOtherRange( range, allRanges ) );
// After the range got transformed, we have an array of ranges. Some of those
// ranges may be "touching" -- they can be next to each other and could be merged.
normalizeRanges( transformed );
// For each `range` from `ranges`, we take only one transformed range.
// This is because we want to prevent situation where single-range selection
// got transformed to multi-range selection. We will take the first range that
// is not in the graveyard.
const newRange = transformed.find(
range => range.root != document.graveyard
);
// `transformedRange` might be `undefined` if transformed range ended up in graveyard.
if ( newRange ) {
selectionRanges.push( newRange );
}
}
// @if CK_DEBUG_ENGINE // console.log( `Restored selection by undo: ${ selectionRanges.join( ', ' ) }` );
// `selectionRanges` may be empty if all ranges ended up in graveyard. If that is the case, do not restore selection.
if ( selectionRanges.length ) {
model.change( writer => {
writer.setSelection( selectionRanges, { backward: isBackward } );
} );
}
}
/**
* Undoes a batch by reversing that batch, transforming reversed batch and finally applying it.
* This is a helper method for {@link #execute}.
*
* @protected
* @param {module:engine/model/batch~Batch} batchToUndo The batch to be undone.
* @param {module:engine/model/batch~Batch} undoingBatch The batch that will contain undoing changes.
*/
_undo( batchToUndo, undoingBatch ) {
const model = this.editor.model;
const document = model.document;
// All changes done by the command execution will be saved as one batch.
this._createdBatches.add( undoingBatch );
const operationsToUndo = batchToUndo.operations.slice().filter( operation => operation.isDocumentOperation );
operationsToUndo.reverse();
// We will process each operation from `batchToUndo`, in reverse order. If there were operations A, B and C in undone batch,
// we need to revert them in reverse order, so first C' (reversed C), then B', then A'.
for ( const operationToUndo of operationsToUndo ) {
const nextBaseVersion = operationToUndo.baseVersion + 1;
const historyOperations = Array.from( document.history.getOperations( nextBaseVersion ) );
const transformedSets = transformSets(
[ operationToUndo.getReversed() ],
historyOperations,
{
useRelations: true,
document: this.editor.model.document,
padWithNoOps: false,
forceWeakRemove: true
}
);
const reversedOperations = transformedSets.operationsA;
// After reversed operation has been transformed by all history operations, apply it.
for ( const operation of reversedOperations ) {
// Before applying, add the operation to the `undoingBatch`.
undoingBatch.addOperation( operation );
model.applyOperation( operation );
document.history.setOperationAsUndone( operationToUndo, operation );
}
}
}
}
// Normalizes list of ranges by joining intersecting or "touching" ranges.
//
// @param {Array.<module:engine/model/range~Range>} ranges
//
function normalizeRanges( ranges ) {
ranges.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 );
for ( let i = 1; i < ranges.length; i++ ) {
const previousRange = ranges[ i - 1 ];
const joinedRange = previousRange.getJoined( ranges[ i ], true );
if ( joinedRange ) {
// Replace the ranges on the list with the new joined range.
i--;
ranges.splice( i, 2, joinedRange );
}
}
}
function isRangeContainedByAnyOtherRange( range, ranges ) {
return ranges.some( otherRange => otherRange !== range && otherRange.containsRange( range, true ) );
}