UNPKG

@ckeditor/ckeditor5-engine

Version:

The editing engine of CKEditor 5 – the best browser-based rich text editor.

184 lines (183 loc) 7.92 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module engine/model/operation/mergeoperation */ import { Operation } from './operation.js'; import { SplitOperation } from './splitoperation.js'; import { ModelPosition } from '../position.js'; import { ModelRange } from '../range.js'; import { _move } from './utils.js'; import { CKEditorError } from '@ckeditor/ckeditor5-utils'; /** * Operation to merge two {@link module:engine/model/element~ModelElement elements}. * * The merged element is the parent of {@link ~MergeOperation#sourcePosition} and it is merged into the parent of * {@link ~MergeOperation#targetPosition}. All nodes from the merged element are moved to {@link ~MergeOperation#targetPosition}. * * The merged element is moved to the graveyard at {@link ~MergeOperation#graveyardPosition}. */ export class MergeOperation extends Operation { /** * Position inside the merged element. All nodes from that element after that position will be moved to {@link #targetPosition}. */ sourcePosition; /** * Summary offset size of nodes which will be moved from the merged element to the new parent. */ howMany; /** * Position which the nodes from the merged elements will be moved to. */ targetPosition; /** * Position in graveyard to which the merged element will be moved. */ graveyardPosition; /** * Creates a merge operation. * * @param sourcePosition Position inside the merged element. All nodes from that * element after that position will be moved to {@link #targetPosition}. * @param howMany Summary offset size of nodes which will be moved from the merged element to the new parent. * @param targetPosition Position which the nodes from the merged elements will be moved to. * @param graveyardPosition Position in graveyard to which the merged element will be moved. * @param baseVersion Document {@link module:engine/model/document~ModelDocument#version} on which operation * can be applied or `null` if the operation operates on detached (non-document) tree. */ constructor(sourcePosition, howMany, targetPosition, graveyardPosition, baseVersion) { super(baseVersion); this.sourcePosition = sourcePosition.clone(); // This is, and should always remain, the first position in its parent. this.sourcePosition.stickiness = 'toPrevious'; this.howMany = howMany; this.targetPosition = targetPosition.clone(); // Except of a rare scenario in `MergeOperation` x `MergeOperation` transformation, // this is, and should always remain, the last position in its parent. this.targetPosition.stickiness = 'toNext'; this.graveyardPosition = graveyardPosition.clone(); } /** * @inheritDoc */ get type() { return 'merge'; } /** * Position before the merged element (which will be deleted). */ get deletionPosition() { return new ModelPosition(this.sourcePosition.root, this.sourcePosition.path.slice(0, -1)); } /** * Artificial range that contains all the nodes from the merged element that will be moved to {@link ~MergeOperation#sourcePosition}. * The range starts at {@link ~MergeOperation#sourcePosition} and ends in the same parent, at `POSITIVE_INFINITY` offset. */ get movedRange() { const end = this.sourcePosition.getShiftedBy(Number.POSITIVE_INFINITY); return new ModelRange(this.sourcePosition, end); } /** * @inheritDoc */ get affectedSelectable() { const mergedElement = this.sourcePosition.parent; return [ ModelRange._createOn(mergedElement), // These could be positions but `Selectable` type only supports `Iterable<Range>`. ModelRange._createFromPositionAndShift(this.targetPosition, 0), ModelRange._createFromPositionAndShift(this.graveyardPosition, 0) ]; } /** * Creates and returns an operation that has the same parameters as this operation. */ clone() { return new MergeOperation(this.sourcePosition, this.howMany, this.targetPosition, this.graveyardPosition, this.baseVersion); } /** * See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}. */ getReversed() { // Positions in this method are transformed by this merge operation because the split operation bases on // the context after this merge operation happened (because split operation reverses it). // So we need to acknowledge that the merge operation happened and those positions changed a little. const targetPosition = this.targetPosition._getTransformedByMergeOperation(this); const path = this.sourcePosition.path.slice(0, -1); const insertionPosition = new ModelPosition(this.sourcePosition.root, path)._getTransformedByMergeOperation(this); return new SplitOperation(targetPosition, this.howMany, insertionPosition, this.graveyardPosition, this.baseVersion + 1); } /** * @inheritDoc * @internal */ _validate() { const sourceElement = this.sourcePosition.parent; const targetElement = this.targetPosition.parent; // Validate whether merge operation has correct parameters. if (!sourceElement.parent) { /** * Merge source position is invalid. The element to be merged must have a parent node. * * @error merge-operation-source-position-invalid */ throw new CKEditorError('merge-operation-source-position-invalid', this); } else if (!targetElement.parent) { /** * Merge target position is invalid. The element to be merged must have a parent node. * * @error merge-operation-target-position-invalid */ throw new CKEditorError('merge-operation-target-position-invalid', this); } else if (this.howMany != sourceElement.maxOffset) { /** * Merge operation specifies wrong number of nodes to move. * * @error merge-operation-how-many-invalid */ throw new CKEditorError('merge-operation-how-many-invalid', this); } } /** * @inheritDoc * @internal */ _execute() { const mergedElement = this.sourcePosition.parent; const sourceRange = ModelRange._createIn(mergedElement); _move(sourceRange, this.targetPosition); _move(ModelRange._createOn(mergedElement), this.graveyardPosition); } /** * @inheritDoc */ toJSON() { const json = super.toJSON(); json.sourcePosition = json.sourcePosition.toJSON(); json.targetPosition = json.targetPosition.toJSON(); json.graveyardPosition = json.graveyardPosition.toJSON(); return json; } /** * @inheritDoc */ static get className() { return 'MergeOperation'; } /** * Creates `MergeOperation` object from deserialized object, i.e. from parsed JSON string. * * @param json Deserialized JSON object. * @param document Document on which this operation will be applied. */ static fromJSON(json, document) { const sourcePosition = ModelPosition.fromJSON(json.sourcePosition, document); const targetPosition = ModelPosition.fromJSON(json.targetPosition, document); const graveyardPosition = ModelPosition.fromJSON(json.graveyardPosition, document); return new this(sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion); } }