@ckeditor/ckeditor5-engine
Advanced tools
Comparing version 19.0.1 to 20.0.0
{ | ||
"name": "@ckeditor/ckeditor5-engine", | ||
"version": "19.0.1", | ||
"version": "20.0.0", | ||
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.", | ||
@@ -24,24 +24,24 @@ "keywords": [ | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-utils": "^19.0.1", | ||
"lodash-es": "^4.17.10" | ||
"@ckeditor/ckeditor5-utils": "^20.0.0", | ||
"lodash-es": "^4.17.15" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-basic-styles": "^19.0.1", | ||
"@ckeditor/ckeditor5-block-quote": "^19.0.1", | ||
"@ckeditor/ckeditor5-core": "^19.0.1", | ||
"@ckeditor/ckeditor5-editor-classic": "^19.0.1", | ||
"@ckeditor/ckeditor5-enter": "^19.0.1", | ||
"@ckeditor/ckeditor5-essentials": "^19.0.1", | ||
"@ckeditor/ckeditor5-heading": "^19.0.1", | ||
"@ckeditor/ckeditor5-link": "^19.0.1", | ||
"@ckeditor/ckeditor5-list": "^19.0.1", | ||
"@ckeditor/ckeditor5-paragraph": "^19.1.0", | ||
"@ckeditor/ckeditor5-table": "^19.1.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^19.1.0", | ||
"@ckeditor/ckeditor5-typing": "^19.0.1", | ||
"@ckeditor/ckeditor5-undo": "^19.0.1", | ||
"@ckeditor/ckeditor5-widget": "^19.1.0" | ||
"@ckeditor/ckeditor5-basic-styles": "^20.0.0", | ||
"@ckeditor/ckeditor5-block-quote": "^20.0.0", | ||
"@ckeditor/ckeditor5-core": "^20.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^20.0.0", | ||
"@ckeditor/ckeditor5-enter": "^20.0.0", | ||
"@ckeditor/ckeditor5-essentials": "^20.0.0", | ||
"@ckeditor/ckeditor5-heading": "^20.0.0", | ||
"@ckeditor/ckeditor5-link": "^20.0.0", | ||
"@ckeditor/ckeditor5-list": "^20.0.0", | ||
"@ckeditor/ckeditor5-paragraph": "^20.0.0", | ||
"@ckeditor/ckeditor5-table": "^20.0.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^20.0.0", | ||
"@ckeditor/ckeditor5-typing": "^20.0.0", | ||
"@ckeditor/ckeditor5-undo": "^20.0.0", | ||
"@ckeditor/ckeditor5-widget": "^20.0.0" | ||
}, | ||
"engines": { | ||
"node": ">=8.0.0", | ||
"node": ">=12.0.0", | ||
"npm": ">=5.7.1" | ||
@@ -48,0 +48,0 @@ }, |
@@ -235,3 +235,3 @@ /** | ||
* Creates a copy of this element and returns it. Created element has the same name and attributes as the original element. | ||
* If clone is deep, the original element's children are also cloned. If not, then empty element is removed. | ||
* If clone is deep, the original element's children are also cloned. If not, then empty element is returned. | ||
* | ||
@@ -238,0 +238,0 @@ * @protected |
@@ -554,5 +554,6 @@ /** | ||
* @param {Boolean} [options.ignoreWhitespaces] Whether text node with whitespaces only should be considered empty. | ||
* @param {Boolean} [options.ignoreMarkers] Whether markers should be ignored. | ||
* @returns {Boolean} | ||
*/ | ||
hasContent( rangeOrElement, options ) { | ||
hasContent( rangeOrElement, options = {} ) { | ||
const range = rangeOrElement instanceof ModelElement ? ModelRange._createIn( rangeOrElement ) : rangeOrElement; | ||
@@ -564,11 +565,13 @@ | ||
const { ignoreWhitespaces = false, ignoreMarkers = false } = options; | ||
// Check if there are any markers which affects data in this given range. | ||
for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( range ) ) { | ||
if ( intersectingMarker.affectsData ) { | ||
return true; | ||
if ( !ignoreMarkers ) { | ||
for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( range ) ) { | ||
if ( intersectingMarker.affectsData ) { | ||
return true; | ||
} | ||
} | ||
} | ||
const { ignoreWhitespaces = false } = options || {}; | ||
for ( const item of range.getItems() ) { | ||
@@ -575,0 +578,0 @@ if ( item.is( 'textProxy' ) ) { |
@@ -29,5 +29,5 @@ /** | ||
* | ||
* * {@glink framework/guides/architecture/editing-engine#schema "Schema"} section of the | ||
* {@glink framework/guides/architecture/editing-engine Introduction to the "Editing engine architecture"}. | ||
* * {@glink framework/guides/deep-dive/schema "Schema" deep dive} guide. | ||
* * {@glink framework/guides/architecture/editing-engine#schema Schema} section of the | ||
* {@glink framework/guides/architecture/editing-engine Introduction to the Editing engine architecture}. | ||
* * {@glink framework/guides/deep-dive/schema Schema deep dive} guide. | ||
* | ||
@@ -175,3 +175,3 @@ * @mixes module:utils/observablemixin~ObservableMixin | ||
/** | ||
* Returns a definition of the given item or `undefined` if item is not registered. | ||
* Returns a definition of the given item or `undefined` if an item is not registered. | ||
* | ||
@@ -217,3 +217,3 @@ * This method should normally be used for reflection purposes (e.g. defining a clone of a certain element, | ||
* Returns `true` if the given item is defined to be | ||
* a block by {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property. | ||
* a block by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property. | ||
* | ||
@@ -226,3 +226,3 @@ * schema.isBlock( 'paragraph' ); // -> true | ||
* | ||
* See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the "Schema" deep dive} | ||
* See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive} | ||
* guide for more details. | ||
@@ -241,7 +241,7 @@ * | ||
* | ||
* It considers the item to be a limit element if its | ||
* It considers an item to be a limit element if its | ||
* {@link module:engine/model/schema~SchemaItemDefinition}'s | ||
* {@link module:engine/model/schema~SchemaItemDefinition#isLimit `isLimit`} or | ||
* {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property | ||
* were set to `true`. | ||
* was set to `true`. | ||
* | ||
@@ -253,3 +253,3 @@ * schema.isLimit( 'paragraph' ); // -> false | ||
* | ||
* See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the "Schema" deep dive} | ||
* See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive} | ||
* guide for more details. | ||
@@ -270,8 +270,8 @@ * | ||
/** | ||
* Returns `true` if the given item is should be treated as an object element. | ||
* Returns `true` if the given item should be treated as an object element. | ||
* | ||
* It considers the item to be an object element if its | ||
* It considers an item to be an object element if its | ||
* {@link module:engine/model/schema~SchemaItemDefinition}'s | ||
* {@link module:engine/model/schema~SchemaItemDefinition#isObject `isObject`} property | ||
* were set to `true`. | ||
* was set to `true`. | ||
* | ||
@@ -284,3 +284,3 @@ * schema.isObject( 'paragraph' ); // -> false | ||
* | ||
* See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the "Schema" deep dive} | ||
* See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive} | ||
* guide for more details. | ||
@@ -298,3 +298,3 @@ * | ||
* Returns `true` if the given item is defined to be | ||
* an inline element by {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property. | ||
* an inline element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property. | ||
* | ||
@@ -307,3 +307,3 @@ * schema.isInline( 'paragraph' ); // -> false | ||
* | ||
* See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the "Schema" deep dive} | ||
* See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive} | ||
* guide for more details. | ||
@@ -750,9 +750,9 @@ * | ||
/** | ||
* Tries to find position ancestors that allows to insert given node. | ||
* Tries to find position ancestors that allow to insert a given node. | ||
* It starts searching from the given position and goes node by node to the top of the model tree | ||
* as long as {@link module:engine/model/schema~Schema#isLimit limit element}, | ||
* {@link module:engine/model/schema~Schema#isObject object element} or top-most ancestor won't be reached. | ||
* as long as a {@link module:engine/model/schema~Schema#isLimit limit element}, an | ||
* {@link module:engine/model/schema~Schema#isObject object element} or a topmost ancestor is not reached. | ||
* | ||
* @param {module:engine/model/position~Position} position Position from searching will start. | ||
* @param {module:engine/model/node~Node|String} node Node for which allowed parent should be found or its name. | ||
* @param {module:engine/model/position~Position} position The position that the search will start from. | ||
* @param {module:engine/model/node~Node|String} node The node for which an allowed parent should be found or its name. | ||
* @returns {module:engine/model/element~Element|null} element Allowed parent or null if nothing was found. | ||
@@ -887,3 +887,3 @@ */ | ||
* @private | ||
* @param {module:engine/model/range~Range} range Range to process. | ||
* @param {module:engine/model/range~Range} range The range to process. | ||
* @param {String} attribute The name of the attribute to check. | ||
@@ -922,3 +922,3 @@ * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed. | ||
* Event fired when the {@link #checkChild} method is called. It allows plugging in | ||
* additional behavior – e.g. implementing rules which cannot be defined using the declarative | ||
* additional behavior, for example implementing rules which cannot be defined using the declarative | ||
* {@link module:engine/model/schema~SchemaItemDefinition} interface. | ||
@@ -932,3 +932,3 @@ * | ||
* {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can | ||
* use this event in a various way, but the most important use case is overriding standard behaviour of the | ||
* use this event in various ways, but the most important use case is overriding standard behavior of the | ||
* `checkChild()` method. Let's see a typical listener template: | ||
@@ -944,3 +944,3 @@ * | ||
* normalized to a {@link module:engine/model/schema~SchemaContext} instance and `child` to a | ||
* {@link module:engine/model/schema~SchemaCompiledItemDefinition} instance, so you don't have to worry about | ||
* {@link module:engine/model/schema~SchemaCompiledItemDefinition} instance, so you do not have to worry about | ||
* the various ways how `context` and `child` may be passed to `checkChild()`. | ||
@@ -950,3 +950,3 @@ * | ||
* | ||
* So, in order to implement a rule "disallow `heading1` in `blockQuote`" you can add such a listener: | ||
* So, in order to implement a rule "disallow `heading1` in `blockQuote`", you can add such a listener: | ||
* | ||
@@ -965,4 +965,4 @@ * schema.on( 'checkChild', ( evt, args ) => { | ||
* | ||
* Allowing elements in specific contexts will be a far less common use case, because it's normally handled by | ||
* `allowIn` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario | ||
* Allowing elements in specific contexts will be a far less common use case, because it is normally handled by the | ||
* `allowIn` rule from {@link module:engine/model/schema~SchemaItemDefinition}. But if you have a complex scenario | ||
* where `listItem` should be allowed only in element `foo` which must be in element `bar`, then this would be the way: | ||
@@ -988,3 +988,3 @@ * | ||
* Event fired when the {@link #checkAttribute} method is called. It allows plugging in | ||
* additional behavior – e.g. implementing rules which cannot be defined using the declarative | ||
* additional behavior, for example implementing rules which cannot be defined using the declarative | ||
* {@link module:engine/model/schema~SchemaItemDefinition} interface. | ||
@@ -996,5 +996,5 @@ * | ||
* | ||
* The {@link #checkAttribute} method fires an event because it's | ||
* The {@link #checkAttribute} method fires an event because it is | ||
* {@link module:utils/observablemixin~ObservableMixin#decorate decorated} with it. Thanks to that you can | ||
* use this event in a various way, but the most important use case is overriding standard behaviour of the | ||
* use this event in various ways, but the most important use case is overriding the standard behavior of the | ||
* `checkAttribute()` method. Let's see a typical listener template: | ||
@@ -1009,6 +1009,6 @@ * | ||
* parameter contains arguments passed to `checkAttribute( context, attributeName )`. However, the `context` parameter is already | ||
* normalized to a {@link module:engine/model/schema~SchemaContext} instance, so you don't have to worry about | ||
* normalized to a {@link module:engine/model/schema~SchemaContext} instance, so you do not have to worry about | ||
* the various ways how `context` may be passed to `checkAttribute()`. | ||
* | ||
* So, in order to implement a rule "disallow `bold` in a text which is in a `heading1` you can add such a listener: | ||
* So, in order to implement a rule "disallow `bold` in a text which is in a `heading1`, you can add such a listener: | ||
* | ||
@@ -1027,4 +1027,4 @@ * schema.on( 'checkAttribute', ( evt, args ) => { | ||
* | ||
* Allowing attributes in specific contexts will be a far less common use case, because it's normally handled by | ||
* `allowAttributes` rule from {@link module:engine/model/schema~SchemaItemDefinition} but if you have a complex scenario | ||
* Allowing attributes in specific contexts will be a far less common use case, because it is normally handled by the | ||
* `allowAttributes` rule from {@link module:engine/model/schema~SchemaItemDefinition}. But if you have a complex scenario | ||
* where `bold` should be allowed only in element `foo` which must be in element `bar`, then this would be the way: | ||
@@ -1080,3 +1080,3 @@ * | ||
* Read more about the meaning of these types in the | ||
* {@glink framework/guides/deep-dive/schema#defining-additional-semantics Dedicated section of the "Schema" deep dive} guide. | ||
* {@glink framework/guides/deep-dive/schema#defining-additional-semantics dedicated section of the Schema deep dive} guide. | ||
* | ||
@@ -1184,3 +1184,3 @@ * # Generic items | ||
* Read more about the block elements in the | ||
* {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the "Schema" deep dive} guide. | ||
* {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive} guide. | ||
* | ||
@@ -1192,3 +1192,3 @@ * @property {Boolean} isInline | ||
* Read more about the inline elements in the | ||
* {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the "Schema" deep dive} guide. | ||
* {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive} guide. | ||
* | ||
@@ -1201,3 +1201,3 @@ * @property {Boolean} isLimit | ||
* Read more about the limit elements in the | ||
* {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the "Schema" deep dive} guide. | ||
* {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive} guide. | ||
* | ||
@@ -1212,3 +1212,3 @@ * @property {Boolean} isObject | ||
* Read more about the object elements in the | ||
* {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the "Schema" deep dive} guide. | ||
* {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive} guide. | ||
*/ | ||
@@ -1215,0 +1215,0 @@ |
@@ -83,4 +83,4 @@ /** | ||
const startPos = selRange.start; | ||
const endPos = LivePosition.fromPosition( selRange.end, 'toNext' ); | ||
// Get the live positions for the range adjusted to span only blocks selected from the user perspective. | ||
const [ startPosition, endPosition ] = getLivePositionsForSelectedBlocks( selRange ); | ||
@@ -101,3 +101,3 @@ // 2. Remove the content if there is any. | ||
if ( !options.leaveUnmerged ) { | ||
mergeBranches( writer, startPos, endPos ); | ||
mergeBranches( writer, startPosition, endPosition ); | ||
@@ -110,6 +110,6 @@ // TMP this will be replaced with a postfixer. | ||
// <h1>Fo{o</h1><p>b}a<b>r</b><p> -> <h1>Fo{}a<b>r</b><h1> -> <h1>Fo{}ar<h1>. | ||
schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer ); | ||
schema.removeDisallowedAttributes( startPosition.parent.getChildren(), writer ); | ||
} | ||
collapseSelectionAt( writer, selection, startPos ); | ||
collapseSelectionAt( writer, selection, startPosition ); | ||
@@ -119,63 +119,240 @@ // 4. Add a paragraph to set selection in it. | ||
// If autoparagraphing is off, we assume that you know what you do so we leave the selection wherever it was. | ||
if ( !options.doNotAutoparagraph && shouldAutoparagraph( schema, startPos ) ) { | ||
insertParagraph( writer, startPos, selection ); | ||
if ( !options.doNotAutoparagraph && shouldAutoparagraph( schema, startPosition ) ) { | ||
insertParagraph( writer, startPosition, selection ); | ||
} | ||
endPos.detach(); | ||
startPosition.detach(); | ||
endPosition.detach(); | ||
} ); | ||
} | ||
// Returns the live positions for the range adjusted to span only blocks selected from the user perspective. Example: | ||
// | ||
// <heading1>[foo</heading1> | ||
// <paragraph>bar</paragraph> | ||
// <heading1>]abc</heading1> <-- this block is not considered as selected | ||
// | ||
// This is the same behavior as in Selection#getSelectedBlocks() "special case". | ||
function getLivePositionsForSelectedBlocks( range ) { | ||
const model = range.root.document.model; | ||
const startPosition = range.start; | ||
let endPosition = range.end; | ||
// If the end of selection is at the start position of last block in the selection, then | ||
// shrink it to not include that trailing block. Note that this should happen only for not empty selection. | ||
if ( model.hasContent( range, { ignoreMarkers: true } ) ) { | ||
const endBlock = getParentBlock( endPosition ); | ||
if ( endBlock && endPosition.isTouching( model.createPositionAt( endBlock, 0 ) ) ) { | ||
// Create forward selection as a probe to find a valid position after excluding last block from the range. | ||
const selection = model.createSelection( range ); | ||
// Modify the forward selection in backward direction to shrink it and remove first position of following block from it. | ||
// This is how modifySelection works and here we are making use of it. | ||
model.modifySelection( selection, { direction: 'backward' } ); | ||
endPosition = selection.getLastPosition(); | ||
} | ||
} | ||
return [ | ||
LivePosition.fromPosition( startPosition, 'toPrevious' ), | ||
LivePosition.fromPosition( endPosition, 'toNext' ) | ||
]; | ||
} | ||
// Finds the lowest element in position's ancestors which is a block. | ||
// Returns null if a limit element is encountered before reaching a block element. | ||
function getParentBlock( position ) { | ||
const element = position.parent; | ||
const schema = element.root.document.model.schema; | ||
const ancestors = element.getAncestors( { parentFirst: true, includeSelf: true } ); | ||
for ( const element of ancestors ) { | ||
if ( schema.isLimit( element ) ) { | ||
return null; | ||
} | ||
if ( schema.isBlock( element ) ) { | ||
return element; | ||
} | ||
} | ||
} | ||
// 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( writer, startPos, endPos ) { | ||
const startParent = startPos.parent; | ||
const endParent = endPos.parent; | ||
function mergeBranches( writer, startPosition, endPosition ) { | ||
const model = writer.model; | ||
// If both positions ended up in the same parent, then there's nothing more to merge: | ||
// <$root><p>x[]</p><p>{}y</p></$root> => <$root><p>xy</p>[]{}</$root> | ||
if ( startParent == endParent ) { | ||
// Verify if there is a need and possibility to merge. | ||
if ( !checkShouldMerge( writer.model.schema, startPosition, endPosition ) ) { | ||
return; | ||
} | ||
// If one of the positions is a limit element, then there's nothing to merge because we don't want to cross the limit boundaries. | ||
if ( writer.model.schema.isLimit( startParent ) || writer.model.schema.isLimit( endParent ) ) { | ||
// If the start element on the common ancestor level is empty, and the end element on the same level is not empty | ||
// then merge those to the right element so that it's properties are preserved (name, attributes). | ||
// Because of OT merging is used instead of removing elements. | ||
// | ||
// Merge left: | ||
// <heading1>foo[</heading1> -> <heading1>foo[]bar</heading1> | ||
// <paragraph>]bar</paragraph> -> --^ | ||
// | ||
// Merge right: | ||
// <heading1>[</heading1> -> | ||
// <paragraph>]bar</paragraph> -> <paragraph>[]bar</paragraph> | ||
// | ||
// Merge left: | ||
// <blockQuote> -> <blockQuote> | ||
// <heading1>foo[</heading1> -> <heading1>foo[]bar</heading1> | ||
// <paragraph>]bar</paragraph> -> --^ | ||
// </blockQuote> -> </blockQuote> | ||
// | ||
// Merge right: | ||
// <blockQuote> -> <blockQuote> | ||
// <heading1>[</heading1> -> | ||
// <paragraph>]bar</paragraph> -> <paragraph>[]bar</paragraph> | ||
// </blockQuote> -> </blockQuote> | ||
// Merging should not go deeper than common ancestor. | ||
const [ startAncestor, endAncestor ] = getAncestorsJustBelowCommonAncestor( startPosition, endPosition ); | ||
if ( !model.hasContent( startAncestor, { ignoreMarkers: true } ) && model.hasContent( endAncestor, { ignoreMarkers: true } ) ) { | ||
mergeBranchesRight( writer, startPosition, endPosition, startAncestor.parent ); | ||
} else { | ||
mergeBranchesLeft( writer, startPosition, endPosition, startAncestor.parent ); | ||
} | ||
} | ||
// Merging blocks to the left (properties of the left block are preserved). | ||
// Simple example: | ||
// <heading1>foo[</heading1> -> <heading1>foo[bar</heading1>] | ||
// <paragraph>]bar</paragraph> -> --^ | ||
// | ||
// Nested example: | ||
// <blockQuote> -> <blockQuote> | ||
// <heading1>foo[</heading1> -> <heading1>foo[bar</heading1> | ||
// </blockQuote> -> </blockQuote>] ^ | ||
// <blockBlock> -> | | ||
// <paragraph>]bar</paragraph> -> --- | ||
// </blockBlock> -> | ||
// | ||
function mergeBranchesLeft( writer, startPosition, endPosition, commonAncestor ) { | ||
const startElement = startPosition.parent; | ||
const endElement = endPosition.parent; | ||
// Merging reached the common ancestor element, stop here. | ||
if ( startElement == commonAncestor || endElement == commonAncestor ) { | ||
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: | ||
// <limit><startParent>x[]</startParent></limit><endParent>{}</endParent> | ||
if ( !checkCanBeMerged( startPos, endPos, writer.model.schema ) ) { | ||
// Remember next positions to merge in next recursive step (also used as modification points pointers). | ||
startPosition = writer.createPositionAfter( startElement ); | ||
endPosition = writer.createPositionBefore( endElement ); | ||
// Move endElement just after startElement if they aren't siblings. | ||
if ( !endPosition.isEqual( startPosition ) ) { | ||
// | ||
// <blockQuote> -> <blockQuote> | ||
// <heading1>foo[</heading1> -> <heading1>foo</heading1>[<paragraph>bar</paragraph> | ||
// </blockQuote> -> </blockQuote> ^ | ||
// <blockBlock> -> <blockBlock> | | ||
// <paragraph>]bar</paragraph> -> ] --- | ||
// </blockBlock> -> </blockBlock> | ||
// | ||
writer.insert( endElement, startPosition ); | ||
} | ||
// Merge two siblings (nodes on sides of startPosition): | ||
// | ||
// <blockQuote> -> <blockQuote> | ||
// <heading1>foo</heading1>[<paragraph>bar</paragraph> -> <heading1>foo[bar</heading1> | ||
// </blockQuote> -> </blockQuote> | ||
// <blockBlock> -> <blockBlock> | ||
// ] -> ] | ||
// </blockBlock> -> </blockBlock> | ||
// | ||
// Or in simple case (without moving elements in above if): | ||
// <heading1>foo</heading1>[<paragraph>bar</paragraph>] -> <heading1>foo[bar</heading1>] | ||
// | ||
writer.merge( startPosition ); | ||
// Remove empty end ancestors: | ||
// | ||
// <blockQuote> -> <blockQuote> | ||
// <heading1>foo[bar</heading1> -> <heading1>foo[bar</heading1> | ||
// </blockQuote> -> </blockQuote> | ||
// <blockBlock> -> | ||
// ] -> ] | ||
// </blockBlock> -> | ||
// | ||
while ( endPosition.parent.isEmpty ) { | ||
const parentToRemove = endPosition.parent; | ||
endPosition = writer.createPositionBefore( parentToRemove ); | ||
writer.remove( parentToRemove ); | ||
} | ||
// Verify if there is a need and possibility to merge next level. | ||
if ( !checkShouldMerge( writer.model.schema, startPosition, endPosition ) ) { | ||
return; | ||
} | ||
// Remember next positions to merge. For example: | ||
// <a><b>x[]</b></a><c><d>{}y</d></c> | ||
// will become: | ||
// <a><b>xy</b>[]</a><c>{}</c> | ||
startPos = writer.createPositionAfter( startParent ); | ||
endPos = writer.createPositionBefore( endParent ); | ||
// Continue merging next level (blockQuote with blockBlock in the examples above if it would not be empty and got removed). | ||
mergeBranchesLeft( writer, startPosition, endPosition, commonAncestor ); | ||
} | ||
if ( !endPos.isEqual( startPos ) ) { | ||
// In this case, before we merge, we need to move `endParent` to the `startPos`: | ||
// <a><b>x[]</b></a><c><d>{}y</d></c> | ||
// becomes: | ||
// <a><b>x</b>[]<d>y</d></a><c>{}</c> | ||
writer.insert( endParent, startPos ); | ||
// Merging blocks to the right (properties of the right block are preserved). | ||
// Simple example: | ||
// <heading1>foo[</heading1> -> --v | ||
// <paragraph>]bar</paragraph> -> [<paragraph>foo]bar</paragraph> | ||
// | ||
// Nested example: | ||
// <blockQuote> -> | ||
// <heading1>foo[</heading1> -> --- | ||
// </blockQuote> -> | | ||
// <blockBlock> -> [<blockBlock> v | ||
// <paragraph>]bar</paragraph> -> <paragraph>foo]bar</paragraph> | ||
// </blockBlock> -> </blockBlock> | ||
// | ||
function mergeBranchesRight( writer, startPosition, endPosition, commonAncestor ) { | ||
const startElement = startPosition.parent; | ||
const endElement = endPosition.parent; | ||
// Merging reached the common ancestor element, stop here. | ||
if ( startElement == commonAncestor || endElement == commonAncestor ) { | ||
return; | ||
} | ||
// Merge two siblings: | ||
// <a>x</a>[]<b>y</b> -> <a>xy</a> (the usual case) | ||
// <a><b>x</b>[]<d>y</d></a><c></c> -> <a><b>xy</b>[]</a><c></c> (this is the "move parent" case shown above) | ||
writer.merge( startPos ); | ||
// Remember next positions to merge in next recursive step (also used as modification points pointers). | ||
startPosition = writer.createPositionAfter( startElement ); | ||
endPosition = writer.createPositionBefore( endElement ); | ||
// Move startElement just before endElement if they aren't siblings. | ||
if ( !endPosition.isEqual( startPosition ) ) { | ||
// | ||
// <blockQuote> -> <blockQuote> | ||
// <heading1>foo[</heading1> -> [ --- | ||
// </blockQuote> -> </blockQuote> | | ||
// <blockBlock> -> <blockBlock> v | ||
// <paragraph>]bar</paragraph> -> <heading1>foo</heading1>]<paragraph>bar</paragraph> | ||
// </blockBlock> -> </blockBlock> | ||
// | ||
writer.insert( startElement, endPosition ); | ||
} | ||
// Remove empty end ancestors: | ||
// <a>fo[o</a><b><a><c>bar]</c></a></b> | ||
// becomes: | ||
// <a>fo[]</a><b><a>{}</a></b> | ||
// So we can remove <a> and <b>. | ||
while ( endPos.parent.isEmpty ) { | ||
const parentToRemove = endPos.parent; | ||
// | ||
// <blockQuote> -> | ||
// [ -> [ | ||
// </blockQuote> -> | ||
// <blockBlock> -> <blockBlock> | ||
// <heading1>foo</heading1>]<paragraph>bar</paragraph> -> <heading1>foo</heading1>]<paragraph>bar</paragraph> | ||
// </blockBlock> -> </blockBlock> | ||
// | ||
while ( startPosition.parent.isEmpty ) { | ||
const parentToRemove = startPosition.parent; | ||
endPos = writer.createPositionBefore( parentToRemove ); | ||
startPosition = writer.createPositionBefore( parentToRemove ); | ||
@@ -185,6 +362,79 @@ writer.remove( parentToRemove ); | ||
// Continue merging next level. | ||
mergeBranches( writer, startPos, endPos ); | ||
// Update endPosition after inserting and removing elements. | ||
endPosition = writer.createPositionBefore( endElement ); | ||
// Merge right two siblings (nodes on sides of endPosition): | ||
// -> | ||
// [ -> [ | ||
// -> | ||
// <blockBlock> -> <blockBlock> | ||
// <heading1>foo</heading1>]<paragraph>bar</paragraph> -> <paragraph>foo]bar</paragraph> | ||
// </blockBlock> -> </blockBlock> | ||
// | ||
// Or in simple case (without moving elements in above if): | ||
// [<heading1>foo</heading1>]<paragraph>bar</paragraph> -> [<heading1>foo]bar</heading1> | ||
// | ||
mergeRight( writer, endPosition ); | ||
// Verify if there is a need and possibility to merge next level. | ||
if ( !checkShouldMerge( writer.model.schema, startPosition, endPosition ) ) { | ||
return; | ||
} | ||
// Continue merging next level (blockQuote with blockBlock in the examples above if it would not be empty and got removed). | ||
mergeBranchesRight( writer, startPosition, endPosition, commonAncestor ); | ||
} | ||
// There is no right merge operation so we need to simulate it. | ||
function mergeRight( writer, position ) { | ||
const startElement = position.nodeBefore; | ||
const endElement = position.nodeAfter; | ||
if ( startElement.name != endElement.name ) { | ||
writer.rename( startElement, endElement.name ); | ||
} | ||
writer.clearAttributes( startElement ); | ||
writer.setAttributes( Object.fromEntries( endElement.getAttributes() ), startElement ); | ||
writer.merge( position ); | ||
} | ||
// Verifies if merging is needed and possible. It's not needed if both positions are in the same element | ||
// and it's not possible if some element is a limit or the range crosses a limit element. | ||
function checkShouldMerge( schema, startPosition, endPosition ) { | ||
const startElement = startPosition.parent; | ||
const endElement = endPosition.parent; | ||
// If both positions ended up in the same parent, then there's nothing more to merge: | ||
// <$root><p>x[</p><p>]y</p></$root> => <$root><p>xy</p>[]</$root> | ||
if ( startElement == endElement ) { | ||
return false; | ||
} | ||
// If one of the positions is a limit element, then there's nothing to merge because we don't want to cross the limit boundaries. | ||
if ( schema.isLimit( startElement ) || schema.isLimit( endElement ) ) { | ||
return false; | ||
} | ||
// Check if operations we'll need to do won't need to cross object or limit boundaries. | ||
// E.g., we can't merge endElement into startElement in this case: | ||
// <limit><startElement>x[</startElement></limit><endElement>]</endElement> | ||
return isCrossingLimitElement( startPosition, endPosition, schema ); | ||
} | ||
// Returns the elements that are the ancestors of the provided positions that are direct children of the common ancestor. | ||
function getAncestorsJustBelowCommonAncestor( positionA, positionB ) { | ||
const ancestorsA = positionA.getAncestors(); | ||
const ancestorsB = positionB.getAncestors(); | ||
let i = 0; | ||
while ( ancestorsA[ i ] && ancestorsA[ i ] == ancestorsB[ i ] ) { | ||
i++; | ||
} | ||
return [ ancestorsA[ i ], ancestorsB[ i ] ]; | ||
} | ||
function shouldAutoparagraph( schema, position ) { | ||
@@ -203,3 +453,3 @@ const isTextAllowed = schema.checkChild( position, '$text' ); | ||
// Usually, widget and caption are marked as objects/limits in the schema, so in this case merging will be blocked. | ||
function checkCanBeMerged( leftPos, rightPos, schema ) { | ||
function isCrossingLimitElement( leftPos, rightPos, schema ) { | ||
const rangeToCheck = new Range( leftPos, rightPos ); | ||
@@ -206,0 +456,0 @@ |
@@ -76,3 +76,3 @@ /** | ||
} else { | ||
writer.append( item._clone( true ), frag ); | ||
writer.append( writer.cloneElement( item, true ), frag ); | ||
} | ||
@@ -79,0 +79,0 @@ } |
@@ -85,3 +85,10 @@ /** | ||
if ( correctedRange ) { | ||
// "Selection fixing" algorithms sometimes get lost. In consequence, it may happen | ||
// that a new range is returned but, in fact, it has the same positions as the original | ||
// range anyway. If this range is not discarded, a new selection will be set and that, | ||
// for instance, would destroy the selection attributes. Let's make sure that the post-fixer | ||
// actually worked first before setting a new selection. | ||
// | ||
// https://github.com/ckeditor/ckeditor5/issues/6693 | ||
if ( correctedRange && !correctedRange.isEqual( modelRange ) ) { | ||
ranges.push( correctedRange ); | ||
@@ -88,0 +95,0 @@ wasFixed = true; |
@@ -120,2 +120,14 @@ /** | ||
/** | ||
* Creates a copy of the element and returns it. Created element has the same name and attributes as the original element. | ||
* If clone is deep, the original element's children are also cloned. If not, then empty element is returned. | ||
* | ||
* @param {module:engine/model/element~Element} element The element to clone. | ||
* @param {Boolean} [deep=true] If set to `true` clones element and all its children recursively. When set to `false`, | ||
* element will be cloned without any child. | ||
*/ | ||
cloneElement( element, deep = true ) { | ||
return element._clone( deep ); | ||
} | ||
/** | ||
* Inserts item on given position. | ||
@@ -122,0 +134,0 @@ * |
@@ -933,2 +933,3 @@ /** | ||
!isText( node1 ) && !isText( node2 ) && | ||
node1.nodeType !== Node.COMMENT_NODE && node2.nodeType !== Node.COMMENT_NODE && | ||
node1.tagName.toLowerCase() === node2.tagName.toLowerCase(); | ||
@@ -935,0 +936,0 @@ } |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1725679
38914
+ Added@ckeditor/ckeditor5-utils@20.0.0(transitive)
- Removed@ckeditor/ckeditor5-utils@19.0.2(transitive)
Updatedlodash-es@^4.17.15