/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ /** * @module engine/controller/deletecontent */ import LivePosition from '../model/liveposition'; import Position from '../model/position'; import Range from '../model/range'; import Element from '../model/element'; /** * Deletes content of the selection and merge siblings. The resulting selection is always collapsed. * * @param {module:engine/model/selection~Selection} selection Selection of which the content should be deleted. * @param {module:engine/model/batch~Batch} batch Batch to which the deltas will be added. * @param {Object} [options] * @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection. * * For example `x[xy]y` will become: * * * `x^y` with the option disabled (`leaveUnmerged == false`) * * `x^y` with enabled (`leaveUnmerged == true`). * * Note: {@link module:engine/model/schema~Schema#objects object} and {@link module:engine/model/schema~Schema#limits limit} * elements will not be merged. * * @param {Boolean} [options.doNotResetEntireContent=false] Whether to skip replacing the entire content with a * paragraph when the entire content was selected. * * For example `[xy] will become: * * * `^` with the option disabled (`doNotResetEntireContent == false`) * * `^` with enabled (`doNotResetEntireContent == true`). */ export default function deleteContent( selection, batch, options = {} ) { if ( selection.isCollapsed ) { return; } const schema = batch.document.schema; // 1. Replace the entire content with paragraph. // See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594. if ( !options.doNotResetEntireContent && shouldEntireContentBeReplacedWithParagraph( schema, selection ) ) { replaceEntireContentWithParagraph( batch, selection ); return; } const selRange = selection.getFirstRange(); const startPos = selRange.start; const endPos = LivePosition.createFromPosition( selRange.end ); // 2. Remove the content if there is any. if ( !selRange.start.isTouching( selRange.end ) ) { batch.remove( selRange ); } // 3. Merge elements in the right branch to the elements in the left branch. // The only reasonable (in terms of data and selection correctness) case in which we need to do that is: // // Fo[]ar => Fo^ar // // However, the algorithm supports also merging deeper structures (up to the depth of the shallower branch), // as it's hard to imagine what should actually be the default behavior. Usually, specific features will // want to override that behavior anyway. if ( !options.leaveUnmerged ) { mergeBranches( batch, startPos, endPos ); // We need to check and strip disallowed attributes in all nested nodes because after merge // some attributes could end up in a path where are disallowed. // // e.g. bold is disallowed for

//

Fo{o

b}ar

->

Fo{}ar

->

Fo{}ar

. schema.removeDisallowedAttributes( startPos.parent.getChildren(), startPos, batch ); } selection.setCollapsedAt( startPos ); // 4. Autoparagraphing. // Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here). if ( shouldAutoparagraph( schema, startPos ) ) { insertParagraph( batch, startPos, selection ); } endPos.detach(); } // This function is a result of reaching the Ballmer's peak for just the right amount of time. // Even I had troubles documenting it after a while and after reading it again I couldn't believe that it really works. function mergeBranches( batch, startPos, endPos ) { const startParent = startPos.parent; const endParent = endPos.parent; // If both positions ended up in the same parent, then there's nothing more to merge: // <$root>

x[]

{}y

=> <$root>

xy

[]{} if ( startParent == endParent ) { return; } // If one of the positions is a root, then there's nothing more to merge (at least in the current state of implementation). // Theoretically in this case we could unwrap the

: <$root>x[]

{}y

, but we don't need to support it yet // so let's just abort. if ( !startParent.parent || !endParent.parent ) { return; } // Check if operations we'll need to do won't need to cross object or limit boundaries. // E.g., we can't merge endParent into startParent in this case: // x[]{} if ( !checkCanBeMerged( startPos, endPos ) ) { return; } // Remember next positions to merge. For example: // x[]{}y // will become: // xy[]{} startPos = Position.createAfter( startParent ); endPos = Position.createBefore( endParent ); if ( endParent.isEmpty ) { batch.remove( endParent ); } else { // At the moment, next startPos is also the position to which the endParent // needs to be moved: // x[]{}y // becomes: // x[]y{} // Move the end parent only if needed. // E.g. not in this case:

ab

[]{}

cd

if ( !endPos.isEqual( startPos ) ) { batch.move( endParent, startPos ); } // To then become: // xy[]{} batch.merge( startPos ); } // Removes empty end ancestors: // fo[obar] // becomes: // fo[]{} // So we can remove and . while ( endPos.parent.isEmpty ) { const parentToRemove = endPos.parent; endPos = Position.createBefore( parentToRemove ); batch.remove( parentToRemove ); } // Continue merging next level. mergeBranches( batch, startPos, endPos ); } function shouldAutoparagraph( schema, position ) { const isTextAllowed = schema.check( { name: '$text', inside: position } ); const isParagraphAllowed = schema.check( { name: 'paragraph', inside: position } ); return !isTextAllowed && isParagraphAllowed; } // Check if parents of two positions can be merged by checking if there are no limit/object // boundaries between those two positions. // // E.g. in

x[]

{} // we'll check

, , and . // Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked. function checkCanBeMerged( leftPos, rightPos ) { const schema = leftPos.root.document.schema; const rangeToCheck = new Range( leftPos, rightPos ); for ( const value of rangeToCheck.getWalker() ) { if ( schema.objects.has( value.item.name ) || schema.limits.has( value.item.name ) ) { return false; } } return true; } function insertParagraph( batch, position, selection ) { const paragraph = new Element( 'paragraph' ); batch.insert( position, paragraph ); selection.setCollapsedAt( paragraph ); } function replaceEntireContentWithParagraph( batch, selection ) { const limitElement = batch.document.schema.getLimitElement( selection ); batch.remove( Range.createIn( limitElement ) ); insertParagraph( batch, Position.createAt( limitElement ), selection ); } // We want to replace the entire content with a paragraph when: // * the entire content is selected, // * selection contains at least two elements, // * whether the paragraph is allowed in schema in the common ancestor. function shouldEntireContentBeReplacedWithParagraph( schema, selection ) { const limitElement = schema.getLimitElement( selection ); if ( !selection.containsEntireContent( limitElement ) ) { return false; } const range = selection.getFirstRange(); if ( range.start.parent == range.end.parent ) { return false; } return schema.check( { name: 'paragraph', inside: limitElement.name } ); }