element is inline and is represented by an attribute in the model.
* // This is why you need to convert only children.
* const { modelRange } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
*
* for ( let item of modelRange.getItems() ) {
* if ( conversionApi.schema.checkAttribute( item, 'linkHref' ) ) {
* conversionApi.writer.setAttribute( 'linkHref', data.viewItem.getAttribute( 'href' ), item );
* }
* }
* }
* } );
*
* // Convert element's font-size style.
* // Note: You should use a low-priority observer in order to ensure that
* // it is executed after the element-to-element converter.
* editor.data.upcastDispatcher.on( 'element:p', ( evt, data, conversionApi ) => {
* const { consumable, schema, writer } = conversionApi;
*
* if ( !consumable.consume( data.viewItem, { style: 'font-size' } ) ) {
* return;
* }
*
* const fontSize = data.viewItem.getStyle( 'font-size' );
*
* // Do not go for the model element after data.modelCursor because it might happen
* // that a single view element was converted to multiple model elements. Get all of them.
* for ( const item of data.modelRange.getItems( { shallow: true } ) ) {
* if ( schema.checkAttribute( item, 'fontSize' ) ) {
* writer.setAttribute( 'fontSize', fontSize, item );
* }
* }
* }, { priority: 'low' } );
*
* // Convert all elements which have no custom converter into a paragraph (autoparagraphing).
* editor.data.upcastDispatcher.on( 'element', ( evt, data, conversionApi ) => {
* // Check if an element can be converted.
* if ( !conversionApi.consumable.test( data.viewItem, { name: data.viewItem.name } ) ) {
* // When an element is already consumed by higher priority converters, do nothing.
* return;
* }
*
* const paragraph = conversionApi.writer.createElement( 'paragraph' );
*
* // Try to safely insert a paragraph at the model cursor - it will find an allowed parent for the current element.
* if ( !conversionApi.safeInsert( paragraph, data.modelCursor ) ) {
* // When an element was not inserted, it means that you cannot insert a paragraph at this position.
* return;
* }
*
* // Consume the inserted element.
* conversionApi.consumable.consume( data.viewItem, { name: data.viewItem.name } ) );
*
* // Convert the children to a paragraph.
* const { modelRange } = conversionApi.convertChildren( data.viewItem, paragraph ) );
*
* // Update `modelRange` and `modelCursor` in the `data` as a conversion result.
* conversionApi.updateConversionResult( paragraph, data );
* }, { priority: 'low' } );
*
* @mixes module:utils/emittermixin~EmitterMixin
* @fires viewCleanup
* @fires element
* @fires text
* @fires documentFragment
*/
class UpcastDispatcher {
/**
* Creates an upcast dispatcher that operates using the passed API.
*
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi
* @param {Object} [conversionApi] Additional properties for an interface that will be passed to events fired
* by the upcast dispatcher.
*/
constructor( conversionApi = {} ) {
/**
* The list of elements that were created during splitting.
*
* After the conversion process the list is cleared.
*
* @private
* @type {Map.>}
*/
this._splitParts = new Map();
/**
* The list of cursor parent elements that were created during splitting.
*
* After the conversion process the list is cleared.
*
* @private
* @type {Map.>}
*/
this._cursorParents = new Map();
/**
* The position in the temporary structure where the converted content is inserted. The structure reflects the context of
* the target position where the content will be inserted. This property is built based on the context parameter of the
* convert method.
*
* @private
* @type {module:engine/model/position~Position|null}
*/
this._modelCursor = null;
/**
* An interface passed by the dispatcher to the event callbacks.
*
* @member {module:engine/conversion/upcastdispatcher~UpcastConversionApi}
*/
this.conversionApi = Object.assign( {}, conversionApi );
// The below methods are bound to this `UpcastDispatcher` instance and set on `conversionApi`.
// This way only a part of `UpcastDispatcher` API is exposed.
this.conversionApi.convertItem = this._convertItem.bind( this );
this.conversionApi.convertChildren = this._convertChildren.bind( this );
this.conversionApi.safeInsert = this._safeInsert.bind( this );
this.conversionApi.updateConversionResult = this._updateConversionResult.bind( this );
// Advanced API - use only if custom position handling is needed.
this.conversionApi.splitToAllowedParent = this._splitToAllowedParent.bind( this );
this.conversionApi.getSplitParts = this._getSplitParts.bind( this );
}
/**
* Starts the conversion process. The entry point for the conversion.
*
* @fires element
* @fires text
* @fires documentFragment
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem
* The part of the view to be converted.
* @param {module:engine/model/writer~Writer} writer An instance of the model writer.
* @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context.
* @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is the result of the conversion process
* wrapped in `DocumentFragment`. Converted marker elements will be set as the document fragment's
* {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}.
*/
convert( viewItem, writer, context = [ '$root' ] ) {
this.fire( 'viewCleanup', viewItem );
// Create context tree and set position in the top element.
// Items will be converted according to this position.
this._modelCursor = createContextTree( context, writer );
// Store writer in conversion as a conversion API
// to be sure that conversion process will use the same batch.
this.conversionApi.writer = writer;
// Create consumable values list for conversion process.
this.conversionApi.consumable = _viewconsumable__WEBPACK_IMPORTED_MODULE_0__["default"].createFrom( viewItem );
// Custom data stored by converter for conversion process.
this.conversionApi.store = {};
// Do the conversion.
const { modelRange } = this._convertItem( viewItem, this._modelCursor );
// Conversion result is always a document fragment so let's create it.
const documentFragment = writer.createDocumentFragment();
// When there is a conversion result.
if ( modelRange ) {
// Remove all empty elements that were create while splitting.
this._removeEmptyElements();
// Move all items that were converted in context tree to the document fragment.
for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) {
writer.append( item, documentFragment );
}
// Extract temporary markers elements from model and set as static markers collection.
documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer );
}
// Clear context position.
this._modelCursor = null;
// Clear split elements & parents lists.
this._splitParts.clear();
this._cursorParents.clear();
// Clear conversion API.
this.conversionApi.writer = null;
this.conversionApi.store = null;
// Return fragment as conversion result.
return documentFragment;
}
/**
* @private
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertItem
*/
_convertItem( viewItem, modelCursor ) {
const data = Object.assign( { viewItem, modelCursor, modelRange: null } );
if ( viewItem.is( 'element' ) ) {
this.fire( 'element:' + viewItem.name, data, this.conversionApi );
} else if ( viewItem.is( '$text' ) ) {
this.fire( 'text', data, this.conversionApi );
} else {
this.fire( 'documentFragment', data, this.conversionApi );
}
// Handle incorrect conversion result.
if ( data.modelRange && !( data.modelRange instanceof _model_range__WEBPACK_IMPORTED_MODULE_1__["default"] ) ) {
/**
* Incorrect conversion result was dropped.
*
* {@link module:engine/model/range~Range Model range} should be a conversion result.
*
* @error view-conversion-dispatcher-incorrect-result
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( 'view-conversion-dispatcher-incorrect-result', this );
}
return { modelRange: data.modelRange, modelCursor: data.modelCursor };
}
/**
* @private
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#convertChildren
*/
_convertChildren( viewItem, elementOrModelCursor ) {
let nextModelCursor = elementOrModelCursor.is( 'position' ) ?
elementOrModelCursor : _model_position__WEBPACK_IMPORTED_MODULE_2__["default"]._createAt( elementOrModelCursor, 0 );
const modelRange = new _model_range__WEBPACK_IMPORTED_MODULE_1__["default"]( nextModelCursor );
for ( const viewChild of Array.from( viewItem.getChildren() ) ) {
const result = this._convertItem( viewChild, nextModelCursor );
if ( result.modelRange instanceof _model_range__WEBPACK_IMPORTED_MODULE_1__["default"] ) {
modelRange.end = result.modelRange.end;
nextModelCursor = result.modelCursor;
}
}
return { modelRange, modelCursor: nextModelCursor };
}
/**
* @private
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#safeInsert
*/
_safeInsert( modelElement, position ) {
// Find allowed parent for element that we are going to insert.
// If current parent does not allow to insert element but one of the ancestors does
// then split nodes to allowed parent.
const splitResult = this._splitToAllowedParent( modelElement, position );
// When there is no split result it means that we can't insert element to model tree, so let's skip it.
if ( !splitResult ) {
return false;
}
// Insert element on allowed position.
this.conversionApi.writer.insert( modelElement, splitResult.position );
return true;
}
/**
* @private
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#updateConversionResult
*/
_updateConversionResult( modelElement, data ) {
const parts = this._getSplitParts( modelElement );
const writer = this.conversionApi.writer;
// Set conversion result range - only if not set already.
if ( !data.modelRange ) {
data.modelRange = writer.createRange(
writer.createPositionBefore( modelElement ),
writer.createPositionAfter( parts[ parts.length - 1 ] )
);
}
const savedCursorParent = this._cursorParents.get( modelElement );
// Now we need to check where the `modelCursor` should be.
if ( savedCursorParent ) {
// If we split parent to insert our element then we want to continue conversion in the new part of the split parent.
//
// before: foo[]
// after: foo []
data.modelCursor = writer.createPositionAt( savedCursorParent, 0 );
} else {
// Otherwise just continue after inserted element.
data.modelCursor = data.modelRange.end;
}
}
/**
* @private
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#splitToAllowedParent
*/
_splitToAllowedParent( node, modelCursor ) {
const { schema, writer } = this.conversionApi;
// Try to find allowed parent.
let allowedParent = schema.findAllowedParent( modelCursor, node );
if ( allowedParent ) {
// When current position parent allows to insert node then return this position.
if ( allowedParent === modelCursor.parent ) {
return { position: modelCursor };
}
// When allowed parent is in context tree (it's outside the converted tree).
if ( this._modelCursor.parent.getAncestors().includes( allowedParent ) ) {
allowedParent = null;
}
}
if ( !allowedParent ) {
// Check if the node wrapped with a paragraph would be accepted by the schema.
if ( !Object(_model_utils_autoparagraphing__WEBPACK_IMPORTED_MODULE_4__["isParagraphable"])( modelCursor, node, schema ) ) {
return null;
}
return {
position: Object(_model_utils_autoparagraphing__WEBPACK_IMPORTED_MODULE_4__["wrapInParagraph"])( modelCursor, writer )
};
}
// Split element to allowed parent.
const splitResult = this.conversionApi.writer.split( modelCursor, allowedParent );
// Using the range returned by `model.Writer#split`, we will pair original elements with their split parts.
//
// The range returned from the writer spans "over the split" or, precisely saying, from the end of the original element (the one
// that got split) to the beginning of the other part of that element:
//
// X[]Y ->
// X[ ]Y
//
// After the split there cannot be any full node between the positions in `splitRange`. The positions are touching.
// Also, because of how splitting works, it is easy to notice, that "closing tags" are in the reverse order than "opening tags".
// Also, since we split all those elements, each of them has to have the other part.
//
// With those observations in mind, we will pair the original elements with their split parts by saving "closing tags" and matching
// them with "opening tags" in the reverse order. For that we can use a stack.
const stack = [];
for ( const treeWalkerValue of splitResult.range.getWalker() ) {
if ( treeWalkerValue.type == 'elementEnd' ) {
stack.push( treeWalkerValue.item );
} else {
// There should not be any text nodes after the element is split, so the only other value is `elementStart`.
const originalPart = stack.pop();
const splitPart = treeWalkerValue.item;
this._registerSplitPair( originalPart, splitPart );
}
}
const cursorParent = splitResult.range.end.parent;
this._cursorParents.set( node, cursorParent );
return {
position: splitResult.position,
cursorParent
};
}
/**
* Registers that a `splitPart` element is a split part of the `originalPart` element.
*
* The data set by this method is used by {@link #_getSplitParts} and {@link #_removeEmptyElements}.
*
* @private
* @param {module:engine/model/element~Element} originalPart
* @param {module:engine/model/element~Element} splitPart
*/
_registerSplitPair( originalPart, splitPart ) {
if ( !this._splitParts.has( originalPart ) ) {
this._splitParts.set( originalPart, [ originalPart ] );
}
const list = this._splitParts.get( originalPart );
this._splitParts.set( splitPart, list );
list.push( splitPart );
}
/**
* @private
* @see module:engine/conversion/upcastdispatcher~UpcastConversionApi#getSplitParts
*/
_getSplitParts( element ) {
let parts;
if ( !this._splitParts.has( element ) ) {
parts = [ element ];
} else {
parts = this._splitParts.get( element );
}
return parts;
}
/**
* Checks if there are any empty elements created while splitting and removes them.
*
* This method works recursively to re-check empty elements again after at least one element was removed in the initial call,
* as some elements might have become empty after other empty elements were removed from them.
*
* @private
*/
_removeEmptyElements() {
let anyRemoved = false;
for ( const element of this._splitParts.keys() ) {
if ( element.isEmpty ) {
this.conversionApi.writer.remove( element );
this._splitParts.delete( element );
anyRemoved = true;
}
}
if ( anyRemoved ) {
this._removeEmptyElements();
}
}
/**
* Fired before the first conversion event, at the beginning of the upcast (view-to-model conversion) process.
*
* @event viewCleanup
* @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element}
* viewItem A part of the view to be converted.
*/
/**
* Fired when an {@link module:engine/view/element~Element} is converted.
*
* `element` is a namespace event for a class of events. Names of actually called events follow the pattern of
* `element:` where `elementName` is the name of the converted element. This way listeners may listen to
* a conversion of all or just specific elements.
*
* @event element
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data The conversion data. Keep in mind that this object is
* shared by reference between all callbacks that will be called. This means that callbacks can override values if needed, and these
* values will be available in other callbacks.
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by the
* callback.
*/
/**
* Fired when a {@link module:engine/view/text~Text} is converted.
*
* @event text
* @see #event:element
*/
/**
* Fired when a {@link module:engine/view/documentfragment~DocumentFragment} is converted.
*
* @event documentFragment
* @see #event:element
*/
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_7__["default"])( UpcastDispatcher, _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_6__["default"] );
// Traverses given model item and searches elements which marks marker range. Found element is removed from
// DocumentFragment but path of this element is stored in a Map which is then returned.
//
// @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/node~Node} modelItem Fragment of model.
// @returns {Map} List of static markers.
function extractMarkersFromModelFragment( modelItem, writer ) {
const markerElements = new Set();
const markers = new Map();
// Create ModelTreeWalker.
const range = _model_range__WEBPACK_IMPORTED_MODULE_1__["default"]._createIn( modelItem ).getItems();
// Walk through DocumentFragment and collect marker elements.
for ( const item of range ) {
// Check if current element is a marker.
if ( item.name == '$marker' ) {
markerElements.add( item );
}
}
// Walk through collected marker elements store its path and remove its from the DocumentFragment.
for ( const markerElement of markerElements ) {
const markerName = markerElement.getAttribute( 'data-name' );
const currentPosition = writer.createPositionBefore( markerElement );
// When marker of given name is not stored it means that we have found the beginning of the range.
if ( !markers.has( markerName ) ) {
markers.set( markerName, new _model_range__WEBPACK_IMPORTED_MODULE_1__["default"]( currentPosition.clone() ) );
// Otherwise is means that we have found end of the marker range.
} else {
markers.get( markerName ).end = currentPosition.clone();
}
// Remove marker element from DocumentFragment.
writer.remove( markerElement );
}
return markers;
}
// Creates model fragment according to given context and returns position in the bottom (the deepest) element.
function createContextTree( contextDefinition, writer ) {
let position;
for ( const item of new _model_schema__WEBPACK_IMPORTED_MODULE_3__["SchemaContext"]( contextDefinition ) ) {
const attributes = {};
for ( const key of item.getAttributeKeys() ) {
attributes[ key ] = item.getAttribute( key );
}
const current = writer.createElement( item.name, attributes );
if ( position ) {
writer.append( current, position );
}
position = _model_position__WEBPACK_IMPORTED_MODULE_2__["default"]._createAt( current, 0 );
}
return position;
}
/**
* A set of conversion utilities available as the third parameter of the
* {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher upcast dispatcher}'s events.
*
* @interface module:engine/conversion/upcastdispatcher~UpcastConversionApi
*/
/**
* Starts the conversion of a given item by firing an appropriate event.
*
* Every fired event is passed (as the first parameter) an object with the `modelRange` property. Every event may set and/or
* modify that property. When all callbacks are done, the final value of the `modelRange` property is returned by this method.
* The `modelRange` must be a {@link module:engine/model/range~Range model range} or `null` (as set by default).
*
* @method #convertItem
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
* @param {module:engine/view/item~Item} viewItem Item to convert.
* @param {module:engine/model/position~Position} modelCursor The conversion position.
* @returns {Object} result The conversion result.
* @returns {module:engine/model/range~Range|null} result.modelRange The model range containing the result of the item conversion,
* created and modified by callbacks attached to the fired event, or `null` if the conversion result was incorrect.
* @returns {module:engine/model/position~Position} result.modelCursor The position where the conversion should be continued.
*/
/**
* Starts the conversion of all children of a given item by firing appropriate events for all the children.
*
* @method #convertChildren
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:text
* @fires module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:documentFragment
* @param {module:engine/view/item~Item} viewItem An element whose children should be converted.
* @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrElement A position or an element of
* the conversion.
* @returns {Object} result The conversion result.
* @returns {module:engine/model/range~Range} result.modelRange The model range containing the results of the conversion of all children
* of the given item. When no child was converted, the range is collapsed.
* @returns {module:engine/model/position~Position} result.modelCursor The position where the conversion should be continued.
*/
/**
* Safely inserts an element to the document, checking the {@link module:engine/model/schema~Schema schema} to find an allowed parent for
* an element that you are going to insert, starting from the given position. If the current parent does not allow to insert the element
* but one of the ancestors does, then splits the nodes to allowed parent.
*
* If the schema allows to insert the node in a given position, nothing is split.
*
* If it was not possible to find an allowed parent, `false` is returned and nothing is split.
*
* Otherwise, ancestors are split.
*
* For instance, if `` is not allowed in `` but is allowed in `$root`:
*
* foo[]bar
*
* -> safe insert for `` will split ->
*
* foo []bar
*
* Example usage:
*
* const myElement = conversionApi.writer.createElement( 'myElement' );
*
* if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
* return;
* }
*
* The split result is saved and {@link #updateConversionResult} should be used to update the
* {@link module:engine/conversion/upcastdispatcher~UpcastConversionData conversion data}.
*
* @method #safeInsert
* @param {module:engine/model/node~Node} node The node to insert.
* @param {module:engine/model/position~Position} position The position where an element is going to be inserted.
* @returns {Boolean} The split result. If it was not possible to find an allowed position, `false` is returned.
*/
/**
* Updates the conversion result and sets a proper {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelRange} and
* the next {@link module:engine/conversion/upcastdispatcher~UpcastConversionData#modelCursor} after the conversion.
* Used together with {@link #safeInsert}, it enables you to easily convert elements without worrying if the node was split
* during the conversion of its children.
*
* A usage example in converter code:
*
* const myElement = conversionApi.writer.createElement( 'myElement' );
*
* if ( !conversionApi.safeInsert( myElement, data.modelCursor ) ) {
* return;
* }
*
* // Children conversion may split `myElement`.
* conversionApi.convertChildren( data.viewItem, myElement );
*
* conversionApi.updateConversionResult( myElement, data );
*
* @method #updateConversionResult
* @param {module:engine/model/element~Element} element
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionData} data Conversion data.
* @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion utilities to be used by the callback.
*/
/**
* Checks the {@link module:engine/model/schema~Schema schema} to find an allowed parent for an element that is going to be inserted
* starting from the given position. If the current parent does not allow inserting an element but one of the ancestors does, the method
* splits nodes to allowed parent.
*
* If the schema allows inserting the node in the given position, nothing is split and an object with that position is returned.
*
* If it was not possible to find an allowed parent, `null` is returned and nothing is split.
*
* Otherwise, ancestors are split and an object with a position and the copy of the split element is returned.
*
* For instance, if `` is not allowed in `` but is allowed in `$root`:
*
* foo[]bar
*
* -> split for `` ->
*
* foo []bar
*
* In the example above, the position between `` elements will be returned as `position` and the second `paragraph`
* as `cursorParent`.
*
* **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
*
* @method #splitToAllowedParent
* @param {module:engine/model/position~Position} position The position where the element is going to be inserted.
* @param {module:engine/model/node~Node} node The node to insert.
* @returns {Object|null} The split result. If it was not possible to find an allowed position, `null` is returned.
* @returns {module:engine/model/position~Position} The position between split elements.
* @returns {module:engine/model/element~Element} [cursorParent] The element inside which the cursor should be placed to
* continue the conversion. When the element is not defined it means that there was no split.
*/
/**
* Returns all the split parts of the given `element` that were created during upcasting through using {@link #splitToAllowedParent}.
* It enables you to easily track these elements and continue processing them after they are split during the conversion of their children.
*
* Foo bar baz ->
* Foo bar baz
*
* For a reference to any of above paragraphs, the function will return all three paragraphs (the original element included),
* sorted in the order of their creation (the original element is the first one).
*
* If the given `element` was not split, an array with a single element is returned.
*
* A usage example in the converter code:
*
* const myElement = conversionApi.writer.createElement( 'myElement' );
*
* // Children conversion may split `myElement`.
* conversionApi.convertChildren( data.viewItem, data.modelCursor );
*
* const splitParts = conversionApi.getSplitParts( myElement );
* const lastSplitPart = splitParts[ splitParts.length - 1 ];
*
* // Setting `data.modelRange` basing on split parts:
* data.modelRange = conversionApi.writer.createRange(
* conversionApi.writer.createPositionBefore( myElement ),
* conversionApi.writer.createPositionAfter( lastSplitPart )
* );
*
* // Setting `data.modelCursor` to continue after the last split element:
* data.modelCursor = conversionApi.writer.createPositionAfter( lastSplitPart );
*
* **Tip:** If you are unable to get a reference to the original element (for example because the code is split into multiple converters
* or even classes) but it has already been converted, you may want to check the first element in `data.modelRange`. This is a common
* situation if an attribute converter is separated from an element converter.
*
* **Note:** This is an advanced method. For most cases {@link #safeInsert} and {@link #updateConversionResult} should be used.
*
* @method #getSplitParts
* @param {module:engine/model/element~Element} element
* @returns {Array.}
*/
/**
* Stores information about what parts of the processed view item are still waiting to be handled. After a piece of view item
* was converted, an appropriate consumable value should be
* {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consumed}.
*
* @member {module:engine/conversion/viewconsumable~ViewConsumable} #consumable
*/
/**
* Custom data stored by converters for the conversion process. Custom properties of this object can be defined and use to
* pass parameters between converters.
*
* The difference between this property and the `data` parameter of
* {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element} is that the `data` parameters allow you
* to pass parameters within a single event and `store` within the whole conversion.
*
* @member {Object} #store
*/
/**
* The model's schema instance.
*
* @member {module:engine/model/schema~Schema} #schema
*/
/**
* The {@link module:engine/model/writer~Writer} instance used to manipulate the data during conversion.
*
* @member {module:engine/model/writer~Writer} #writer
*/
/**
* Conversion data.
*
* **Note:** Keep in mind that this object is shared by reference between all conversion callbacks that will be called.
* This means that callbacks can override values if needed, and these values will be available in other callbacks.
*
* @typedef {Object} module:engine/conversion/upcastdispatcher~UpcastConversionData
*
* @property {module:engine/view/item~Item} viewItem The converted item.
* @property {module:engine/model/position~Position} modelCursor The position where the converter should start changes.
* Change this value for the next converter to tell where the conversion should continue.
* @property {module:engine/model/range~Range} [modelRange] The current state of conversion result. Every change to
* the converted element should be reflected by setting or modifying this property.
*/
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/conversion/upcasthelpers.js":
/*!*********************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/upcasthelpers.js ***!
\*********************************************************************************/
/*! exports provided: default, convertToModelFragment, convertText, convertSelectionChange */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return UpcastHelpers; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertToModelFragment", function() { return convertToModelFragment; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertText", function() { return convertText; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertSelectionChange", function() { return convertSelectionChange; });
/* harmony import */ var _view_matcher__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../view/matcher */ "./node_modules/@ckeditor/ckeditor5-engine/src/view/matcher.js");
/* harmony import */ var _conversionhelpers__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./conversionhelpers */ "./node_modules/@ckeditor/ckeditor5-engine/src/conversion/conversionhelpers.js");
/* harmony import */ var lodash_es__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! lodash-es */ "./node_modules/lodash-es/lodash.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_priorities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/priorities */ "./node_modules/@ckeditor/ckeditor5-utils/src/priorities.js");
/* harmony import */ var _model_utils_autoparagraphing__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../model/utils/autoparagraphing */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/utils/autoparagraphing.js");
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
* {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
*
* @module engine/conversion/upcasthelpers
*/
/**
* Upcast conversion helper functions.
*
* @extends module:engine/conversion/conversionhelpers~ConversionHelpers
*/
class UpcastHelpers extends _conversionhelpers__WEBPACK_IMPORTED_MODULE_1__["default"] {
/**
* View element to model element conversion helper.
*
* This conversion results in creating a model element. For example,
* view `Foo
` becomes `Foo ` in the model.
*
* Keep in mind that the element will be inserted only if it is allowed
* by {@link module:engine/model/schema~Schema schema} configuration.
*
* editor.conversion.for( 'upcast' ).elementToElement( {
* view: 'p',
* model: 'paragraph'
* } );
*
* editor.conversion.for( 'upcast' ).elementToElement( {
* view: 'p',
* model: 'paragraph',
* converterPriority: 'high'
* } );
*
* editor.conversion.for( 'upcast' ).elementToElement( {
* view: {
* name: 'p',
* classes: 'fancy'
* },
* model: 'fancyParagraph'
* } );
*
* editor.conversion.for( 'upcast' ).elementToElement( {
* view: {
* name: 'p',
* classes: 'heading'
* },
* model: ( viewElement, conversionApi ) => {
* const modelWriter = conversionApi.writer;
*
* return modelWriter.createElement( 'heading', { level: viewElement.getAttribute( 'data-level' ) } );
* }
* } );
*
* See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
* to the conversion process.
*
* @method #elementToElement
* @param {Object} config Conversion configuration.
* @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
* set, the converter will fire for every view element.
* @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element instance or a
* function that takes a view element and {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API}
* and returns a model element. The model element will be inserted in the model.
* @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
* @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
elementToElement( config ) {
return this.add( upcastElementToElement( config ) );
}
/**
* View element to model attribute conversion helper.
*
* This conversion results in setting an attribute on a model node. For example, view `Foo ` becomes
* `Foo` {@link module:engine/model/text~Text model text node} with `bold` attribute set to `true`.
*
* This helper is meant to set a model attribute on all the elements that are inside the converted element:
*
* Foo --> Foo
--> <$text bold="true">Foo$text>
*
* Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
* Even though `` is over `` element, `bold="true"` was added to the text. See
* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#attributeToAttribute} for comparison.
*
* Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: 'strong',
* model: 'bold'
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: 'strong',
* model: 'bold',
* converterPriority: 'high'
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: {
* name: 'span',
* classes: 'bold'
* },
* model: 'bold'
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: {
* name: 'span',
* classes: [ 'styled', 'styled-dark' ]
* },
* model: {
* key: 'styled',
* value: 'dark'
* }
* } );
*
* editor.conversion.for( 'upcast' ).elementToAttribute( {
* view: {
* name: 'span',
* styles: {
* 'font-size': /[\s\S]+/
* }
* },
* model: {
* key: 'fontSize',
* value: ( viewElement, conversionApi ) => {
* const fontSize = viewElement.getStyle( 'font-size' );
* const value = fontSize.substr( 0, fontSize.length - 2 );
*
* if ( value <= 10 ) {
* return 'small';
* } else if ( value > 12 ) {
* return 'big';
* }
*
* return null;
* }
* }
* } );
*
* See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
* to the conversion process.
*
* @method #elementToAttribute
* @param {Object} config Conversion configuration.
* @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
* @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
* the model attribute. `value` property may be set as a function that takes a view element and
* {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the value.
* If `String` is given, the model attribute value will be set to `true`.
* @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
* @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
elementToAttribute( config ) {
return this.add( upcastElementToAttribute( config ) );
}
/**
* View attribute to model attribute conversion helper.
*
* This conversion results in setting an attribute on a model node. For example, view ` ` becomes
* ` ` in the model.
*
* This helper is meant to convert view attributes from view elements which got converted to the model, so the view attribute
* is set only on the corresponding model node:
*
*
-->
*
* Above, `class="dark"` attribute is added only to the `` elements that has it. This is in contrary to
* {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToAttribute} which sets attributes for
* all the children in the model:
*
*
Foo -->
Foo
-->
<$text bold="true">Foo$text>
*
* Above is a sample of HTML code, that goes through autoparagraphing (first step) and then is converted (second step).
* Even though `
` is over `` element, `bold="true"` was added to the text.
*
* Keep in mind that the attribute will be set only if it is allowed by {@link module:engine/model/schema~Schema schema} configuration.
*
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: 'src',
* model: 'source'
* } );
*
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: { key: 'src' },
* model: 'source'
* } );
*
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: { key: 'src' },
* model: 'source',
* converterPriority: 'normal'
* } );
*
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: {
* key: 'data-style',
* value: /[\s\S]+/
* },
* model: 'styled'
* } );
*
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: {
* name: 'img',
* key: 'class',
* value: 'styled-dark'
* },
* model: {
* key: 'styled',
* value: 'dark'
* }
* } );
*
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: {
* key: 'class',
* value: /styled-[\S]+/
* },
* model: {
* key: 'styled'
* value: ( viewElement, conversionApi ) => {
* const regexp = /styled-([\S]+)/;
* const match = viewElement.getAttribute( 'class' ).match( regexp );
*
* return match[ 1 ];
* }
* }
* } );
*
* Converting styles works a bit differently as it requires `view.styles` to be an object and by default
* a model attribute will be set to `true` by such a converter. You can set the model attribute to any value by providing the `value`
* callback that returns the desired value.
*
* // Default conversion of font-weight style will result in setting bold attribute to true.
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: {
* styles: {
* 'font-weight': 'bold'
* }
* },
* model: 'bold'
* } );
*
* // This converter will pass any style value to the `lineHeight` model attribute.
* editor.conversion.for( 'upcast' ).attributeToAttribute( {
* view: {
* styles: {
* 'line-height': /[\s\S]+/
* }
* },
* model: {
* key: 'lineHeight',
* value: ( viewElement, conversionApi ) => viewElement.getStyle( 'line-height' )
* }
* } );
*
* See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
* to the conversion process.
*
* @method #attributeToAttribute
* @param {Object} config Conversion configuration.
* @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
* attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
* specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
* property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
* a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
* @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
* the model attribute. `value` property may be set as a function that takes a view element and
* {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the value.
* If `String` is given, the model attribute value will be same as view attribute value.
* @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
* @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
attributeToAttribute( config ) {
return this.add( upcastAttributeToAttribute( config ) );
}
/**
* View element to model marker conversion helper.
*
* **Note**: This method was deprecated. Please use {@link #dataToMarker} instead.
*
* This conversion results in creating a model marker. For example, if the marker was stored in a view as an element:
* `
Fo o
B ar
`,
* after the conversion is done, the marker will be available in
* {@link module:engine/model/model~Model#markers model document markers}.
*
* editor.conversion.for( 'upcast' ).elementToMarker( {
* view: 'marker-search',
* model: 'search'
* } );
*
* editor.conversion.for( 'upcast' ).elementToMarker( {
* view: 'marker-search',
* model: 'search',
* converterPriority: 'high'
* } );
*
* editor.conversion.for( 'upcast' ).elementToMarker( {
* view: 'marker-search',
* model: ( viewElement, conversionApi ) => 'comment:' + viewElement.getAttribute( 'data-comment-id' )
* } );
*
* editor.conversion.for( 'upcast' ).elementToMarker( {
* view: {
* name: 'span',
* attributes: {
* 'data-marker': 'search'
* }
* },
* model: 'search'
* } );
*
* See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
* to the conversion process.
*
* @deprecated
* @method #elementToMarker
* @param {Object} config Conversion configuration.
* @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
* @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
* a model marker name.
* @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
* @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
elementToMarker( config ) {
/**
* The {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker `UpcastHelpers#elementToMarker()`}
* method was deprecated and will be removed in the near future.
* Please use {@link module:engine/conversion/upcasthelpers~UpcastHelpers#dataToMarker `UpcastHelpers#dataToMarker()`} instead.
*
* @error upcast-helpers-element-to-marker-deprecated
*/
Object(_ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__["logWarning"])( 'upcast-helpers-element-to-marker-deprecated' );
return this.add( upcastElementToMarker( config ) );
}
/**
* View-to-model marker conversion helper.
*
* Converts view data created by {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData `#markerToData()`}
* back to a model marker.
*
* This converter looks for specific view elements and view attributes that mark marker boundaries. See
* {@link module:engine/conversion/downcasthelpers~DowncastHelpers#markerToData `#markerToData()`} to learn what view data
* is expected by this converter.
*
* The `config.view` property is equal to the marker group name to convert.
*
* By default, this converter creates markers with the `group:name` name convention (to match the default `markerToData` conversion).
*
* The conversion configuration can take a function that will generate a marker name.
* If such function is set as the `config.model` parameter, it is passed the `name` part from the view element or attribute and it is
* expected to return a string with the marker name.
*
* Basic usage:
*
* // Using the default conversion.
* // In this case, all markers from the `comment` group will be converted.
* // The conversion will look for `` and `` tags and
* // `data-comment-start-before`, `data-comment-start-after`,
* // `data-comment-end-before` and `data-comment-end-after` attributes.
* editor.conversion.for( 'upcast' ).dataToMarker( {
* view: 'comment'
* } );
*
* An example of a model that may be generated by this conversion:
*
* // View:
* Foo bar
*
*
* // Model:
* Foo[bar
* ]
*
* Where `[]` are boundaries of a marker that will receive the `comment:commentId:uid` name.
*
* Other examples of usage:
*
* // Using a custom function which is the same as the default conversion:
* editor.conversion.for( 'upcast' ).dataToMarker( {
* view: 'comment',
* model: ( name, conversionApi ) => 'comment:' + name,
* } );
*
* // Using the converter priority:
* editor.conversion.for( 'upcast' ).dataToMarker( {
* view: 'comment',
* model: ( name, conversionApi ) => 'comment:' + name,
* converterPriority: 'high'
* } );
*
* See {@link module:engine/conversion/conversion~Conversion#for `conversion.for()`} to learn how to add a converter
* to the conversion process.
*
* @method #dataToMarker
* @param {Object} config Conversion configuration.
* @param {String} config.view The marker group name to convert.
* @param {Function} [config.model] A function that takes the `name` part from the view element or attribute and
* {@link module:engine/conversion/upcastdispatcher~UpcastConversionApi upcast conversion API} and returns the marker name.
* @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
* @returns {module:engine/conversion/upcasthelpers~UpcastHelpers}
*/
dataToMarker( config ) {
return this.add( upcastDataToMarker( config ) );
}
}
/**
* Function factory, creates a converter that converts {@link module:engine/view/documentfragment~DocumentFragment view document fragment}
* or all children of {@link module:engine/view/element~Element} into
* {@link module:engine/model/documentfragment~DocumentFragment model document fragment}.
* This is the "entry-point" converter for upcast (view to model conversion). This converter starts the conversion of all children
* of passed view document fragment. Those children {@link module:engine/view/node~Node view nodes} are then handled by other converters.
*
* This also a "default", last resort converter for all view elements that has not been converted by other converters.
* When a view element is being converted to the model but it does not have converter specified, that view element
* will be converted to {@link module:engine/model/documentfragment~DocumentFragment model document fragment} and returned.
*
* @returns {Function} Universal converter for view {@link module:engine/view/documentfragment~DocumentFragment fragments} and
* {@link module:engine/view/element~Element elements} that returns
* {@link module:engine/model/documentfragment~DocumentFragment model fragment} with children of converted view item.
*/
function convertToModelFragment() {
return ( evt, data, conversionApi ) => {
// Second argument in `consumable.consume` is discarded for ViewDocumentFragment but is needed for ViewElement.
if ( !data.modelRange && conversionApi.consumable.consume( data.viewItem, { name: true } ) ) {
const { modelRange, modelCursor } = conversionApi.convertChildren( data.viewItem, data.modelCursor );
data.modelRange = modelRange;
data.modelCursor = modelCursor;
}
};
}
/**
* Function factory, creates a converter that converts {@link module:engine/view/text~Text} to {@link module:engine/model/text~Text}.
*
* @returns {Function} {@link module:engine/view/text~Text View text} converter.
*/
function convertText() {
return ( evt, data, { schema, consumable, writer } ) => {
let position = data.modelCursor;
// When node is already converted then do nothing.
if ( !consumable.test( data.viewItem ) ) {
return;
}
if ( !schema.checkChild( position, '$text' ) ) {
if ( !Object(_model_utils_autoparagraphing__WEBPACK_IMPORTED_MODULE_5__["isParagraphable"])( position, '$text', schema ) ) {
return;
}
position = Object(_model_utils_autoparagraphing__WEBPACK_IMPORTED_MODULE_5__["wrapInParagraph"])( position, writer );
}
consumable.consume( data.viewItem );
const text = writer.createText( data.viewItem.data );
writer.insert( text, position );
data.modelRange = writer.createRange(
position,
position.getShiftedBy( text.offsetSize )
);
data.modelCursor = data.modelRange.end;
};
}
/**
* Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection
* view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event
* and sets in on the {@link module:engine/model/document~Document#selection model}.
*
* **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model
* conversion mechanism, the callback should be set directly on view document.
*
* view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) );
*
* @param {module:engine/model/model~Model} model Data model.
* @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper.
* @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function.
*/
function convertSelectionChange( model, mapper ) {
return ( evt, data ) => {
const viewSelection = data.newSelection;
const ranges = [];
for ( const viewRange of viewSelection.getRanges() ) {
ranges.push( mapper.toModelRange( viewRange ) );
}
const modelSelection = model.createSelection( ranges, { backward: viewSelection.isBackward } );
if ( !modelSelection.isEqual( model.document.selection ) ) {
model.change( writer => {
writer.setSelection( modelSelection );
} );
}
};
}
// View element to model element conversion helper.
//
// See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {module:engine/view/matcher~MatcherPattern} [config.view] Pattern matching all view elements which should be converted. If not
// set, the converter will fire for every view element.
// @param {String|module:engine/model/element~Element|Function} config.model Name of the model element, a model element
// instance or a function that takes a view element and returns a model element. The model element will be inserted in the model.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function upcastElementToElement( config ) {
config = Object(lodash_es__WEBPACK_IMPORTED_MODULE_2__["cloneDeep"])( config );
const converter = prepareToElementConverter( config );
const elementName = getViewElementNameFromConfig( config.view );
const eventName = elementName ? 'element:' + elementName : 'element';
return dispatcher => {
dispatcher.on( eventName, converter, { priority: config.converterPriority || 'normal' } );
};
}
// View element to model attribute conversion helper.
//
// See {@link ~UpcastHelpers#elementToAttribute `.elementToAttribute()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
// @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
// the model attribute. `value` property may be set as a function that takes a view element and returns the value.
// If `String` is given, the model attribute value will be set to `true`.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
// @returns {Function} Conversion helper.
function upcastElementToAttribute( config ) {
config = Object(lodash_es__WEBPACK_IMPORTED_MODULE_2__["cloneDeep"])( config );
normalizeModelAttributeConfig( config );
const converter = prepareToAttributeConverter( config, false );
const elementName = getViewElementNameFromConfig( config.view );
const eventName = elementName ? 'element:' + elementName : 'element';
return dispatcher => {
dispatcher.on( eventName, converter, { priority: config.converterPriority || 'low' } );
};
}
// View attribute to model attribute conversion helper.
//
// See {@link ~UpcastHelpers#attributeToAttribute `.attributeToAttribute()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {String|Object} config.view Specifies which view attribute will be converted. If a `String` is passed,
// attributes with given key will be converted. If an `Object` is passed, it must have a required `key` property,
// specifying view attribute key, and may have an optional `value` property, specifying view attribute value and optional `name`
// property specifying a view element name from/on which the attribute should be converted. `value` can be given as a `String`,
// a `RegExp` or a function callback, that takes view attribute value as the only parameter and returns `Boolean`.
// @param {String|Object} config.model Model attribute key or an object with `key` and `value` properties, describing
// the model attribute. `value` property may be set as a function that takes a view element and returns the value.
// If `String` is given, the model attribute value will be same as view attribute value.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='low'] Converter priority.
// @returns {Function} Conversion helper.
function upcastAttributeToAttribute( config ) {
config = Object(lodash_es__WEBPACK_IMPORTED_MODULE_2__["cloneDeep"])( config );
let viewKey = null;
if ( typeof config.view == 'string' || config.view.key ) {
viewKey = normalizeViewAttributeKeyValueConfig( config );
}
normalizeModelAttributeConfig( config, viewKey );
const converter = prepareToAttributeConverter( config, true );
return dispatcher => {
dispatcher.on( 'element', converter, { priority: config.converterPriority || 'low' } );
};
}
// View element to model marker conversion helper.
//
// See {@link ~UpcastHelpers#elementToMarker `.elementToMarker()` upcast helper} for examples.
//
// @param {Object} config Conversion configuration.
// @param {module:engine/view/matcher~MatcherPattern} config.view Pattern matching all view elements which should be converted.
// @param {String|Function} config.model Name of the model marker, or a function that takes a view element and returns
// a model marker name.
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal'] Converter priority.
// @returns {Function} Conversion helper.
function upcastElementToMarker( config ) {
config = Object(lodash_es__WEBPACK_IMPORTED_MODULE_2__["cloneDeep"])( config );
normalizeElementToMarkerConfig( config );
return upcastElementToElement( config );
}
// View data to model marker conversion helper.
//
// See {@link ~UpcastHelpers#dataToMarker} to learn more.
//
// @param {Object} config
// @param {String} config.view
// @param {Function} [config.model]
// @param {module:utils/priorities~PriorityString} [config.converterPriority='normal']
// @returns {Function} Conversion helper.
function upcastDataToMarker( config ) {
config = Object(lodash_es__WEBPACK_IMPORTED_MODULE_2__["cloneDeep"])( config );
// Default conversion.
if ( !config.model ) {
config.model = name => {
return name ? config.view + ':' + name : config.view;
};
}
const converterStart = prepareToElementConverter( normalizeDataToMarkerConfig( config, 'start' ) );
const converterEnd = prepareToElementConverter( normalizeDataToMarkerConfig( config, 'end' ) );
return dispatcher => {
dispatcher.on( 'element:' + config.view + '-start', converterStart, { priority: config.converterPriority || 'normal' } );
dispatcher.on( 'element:' + config.view + '-end', converterEnd, { priority: config.converterPriority || 'normal' } );
// Below is a hack that is needed to properly handle `converterPriority` for both elements and attributes.
// Attribute conversion needs to be performed *after* element conversion.
// This converter handles both element conversion and attribute conversion, which means that if a single
// `config.converterPriority` is used, it will lead to problems. For example, if `'high'` priority is used,
// then attribute conversion will be performed before a lot of element upcast converters.
// On the other hand we want to support `config.converterPriority` and overwriting conveters.
//
// To have it work, we need to do some extra processing for priority for attribute converter.
// Priority `'low'` value should be the base value and then we will change it depending on `config.converterPriority` value.
//
// This hack probably would not be needed if attributes are upcasted separately.
//
const basePriority = _ckeditor_ckeditor5_utils_src_priorities__WEBPACK_IMPORTED_MODULE_4__["default"].get( 'low' );
const maxPriority = _ckeditor_ckeditor5_utils_src_priorities__WEBPACK_IMPORTED_MODULE_4__["default"].get( 'highest' );
const priorityFactor = _ckeditor_ckeditor5_utils_src_priorities__WEBPACK_IMPORTED_MODULE_4__["default"].get( config.converterPriority ) / maxPriority; // Number in range [ -1, 1 ].
dispatcher.on( 'element', upcastAttributeToMarker( config ), { priority: basePriority + priorityFactor } );
};
}
// Function factory, returns a callback function which converts view attributes to a model marker.
//
// The converter looks for elements with `data-group-start-before`, `data-group-start-after`, `data-group-end-before`
// and `data-group-end-after` attributes and inserts `$marker` model elements before/after those elements.
// `group` part is specified in `config.view`.
//
// @param {Object} config
// @param {String} config.view
// @param {Function} [config.model]
// @returns {Function} Marker converter.
function upcastAttributeToMarker( config ) {
return ( evt, data, conversionApi ) => {
const attrName = `data-${ config.view }`;
// This converter wants to add a model element, marking a marker, before/after an element (or maybe even group of elements).
// To do that, we can use `data.modelRange` which is set on an element (or a group of elements) that has been upcasted.
// But, if the processed view element has not been upcasted yet (it does not have been converted), we need to
// fire conversion for its children first, then we will have `data.modelRange` available.
if ( !data.modelRange ) {
data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
}
if ( conversionApi.consumable.consume( data.viewItem, { attributes: attrName + '-end-after' } ) ) {
addMarkerElements( data.modelRange.end, data.viewItem.getAttribute( attrName + '-end-after' ).split( ',' ) );
}
if ( conversionApi.consumable.consume( data.viewItem, { attributes: attrName + '-start-after' } ) ) {
addMarkerElements( data.modelRange.end, data.viewItem.getAttribute( attrName + '-start-after' ).split( ',' ) );
}
if ( conversionApi.consumable.consume( data.viewItem, { attributes: attrName + '-end-before' } ) ) {
addMarkerElements( data.modelRange.start, data.viewItem.getAttribute( attrName + '-end-before' ).split( ',' ) );
}
if ( conversionApi.consumable.consume( data.viewItem, { attributes: attrName + '-start-before' } ) ) {
addMarkerElements( data.modelRange.start, data.viewItem.getAttribute( attrName + '-start-before' ).split( ',' ) );
}
function addMarkerElements( position, markerViewNames ) {
for ( const markerViewName of markerViewNames ) {
const markerName = config.model( markerViewName, conversionApi );
const element = conversionApi.writer.createElement( '$marker', { 'data-name': markerName } );
conversionApi.writer.insert( element, position );
if ( data.modelCursor.isEqual( position ) ) {
data.modelCursor = data.modelCursor.getShiftedBy( 1 );
} else {
data.modelCursor = data.modelCursor._getTransformedByInsertion( position, 1 );
}
data.modelRange = data.modelRange._getTransformedByInsertion( position, 1 )[ 0 ];
}
}
};
}
// Helper function for from-view-element conversion. Checks if `config.view` directly specifies converted view element's name
// and if so, returns it.
//
// @param {Object} config Conversion view config.
// @returns {String|null} View element name or `null` if name is not directly set.
function getViewElementNameFromConfig( viewConfig ) {
if ( typeof viewConfig == 'string' ) {
return viewConfig;
}
if ( typeof viewConfig == 'object' && typeof viewConfig.name == 'string' ) {
return viewConfig.name;
}
return null;
}
// Helper for to-model-element conversion. Takes a config object and returns a proper converter function.
//
// @param {Object} config Conversion configuration.
// @returns {Function} View to model converter.
function prepareToElementConverter( config ) {
const matcher = new _view_matcher__WEBPACK_IMPORTED_MODULE_0__["default"]( config.view );
return ( evt, data, conversionApi ) => {
const matcherResult = matcher.match( data.viewItem );
if ( !matcherResult ) {
return;
}
const match = matcherResult.match;
// Force consuming element's name.
match.name = true;
if ( !conversionApi.consumable.test( data.viewItem, match ) ) {
return;
}
const modelElement = getModelElement( config.model, data.viewItem, conversionApi );
if ( !modelElement ) {
return;
}
if ( !conversionApi.safeInsert( modelElement, data.modelCursor ) ) {
return;
}
conversionApi.consumable.consume( data.viewItem, match );
conversionApi.convertChildren( data.viewItem, modelElement );
conversionApi.updateConversionResult( modelElement, data );
};
}
// Helper function for upcasting-to-element converter. Takes the model configuration, the converted view element
// and a writer instance and returns a model element instance to be inserted in the model.
//
// @param {String|Function|module:engine/model/element~Element} model Model conversion configuration.
// @param {module:engine/view/node~Node} input The converted view node.
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi The upcast conversion API.
function getModelElement( model, input, conversionApi ) {
if ( model instanceof Function ) {
return model( input, conversionApi );
} else {
return conversionApi.writer.createElement( model );
}
}
// Helper function view-attribute-to-model-attribute helper. Normalizes `config.view` which was set as `String` or
// as an `Object` with `key`, `value` and `name` properties. Normalized `config.view` has is compatible with
// {@link module:engine/view/matcher~MatcherPattern}.
//
// @param {Object} config Conversion config.
// @returns {String} Key of the converted view attribute.
function normalizeViewAttributeKeyValueConfig( config ) {
if ( typeof config.view == 'string' ) {
config.view = { key: config.view };
}
const key = config.view.key;
let normalized;
if ( key == 'class' || key == 'style' ) {
const keyName = key == 'class' ? 'classes' : 'styles';
normalized = {
[ keyName ]: config.view.value
};
} else {
const value = typeof config.view.value == 'undefined' ? /[\s\S]*/ : config.view.value;
normalized = {
attributes: {
[ key ]: value
}
};
}
if ( config.view.name ) {
normalized.name = config.view.name;
}
config.view = normalized;
return key;
}
// Helper function that normalizes `config.model` in from-model-attribute conversion. `config.model` can be set
// as a `String`, an `Object` with only `key` property or an `Object` with `key` and `value` properties. Normalized
// `config.model` is an `Object` with `key` and `value` properties.
//
// @param {Object} config Conversion config.
// @param {String} viewAttributeKeyToCopy Key of the converted view attribute. If it is set, model attribute value
// will be equal to view attribute value.
function normalizeModelAttributeConfig( config, viewAttributeKeyToCopy = null ) {
const defaultModelValue = viewAttributeKeyToCopy === null ? true : viewElement => viewElement.getAttribute( viewAttributeKeyToCopy );
const key = typeof config.model != 'object' ? config.model : config.model.key;
const value = typeof config.model != 'object' || typeof config.model.value == 'undefined' ? defaultModelValue : config.model.value;
config.model = { key, value };
}
// Helper for to-model-attribute conversion. Takes the model attribute name and conversion configuration and returns
// a proper converter function.
//
// @param {String} modelAttributeKey The key of the model attribute to set on a model node.
// @param {Object|Array.} config Conversion configuration. It is possible to provide multiple configurations in an array.
// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
// on all elements in the range.
function prepareToAttributeConverter( config, shallow ) {
const matcher = new _view_matcher__WEBPACK_IMPORTED_MODULE_0__["default"]( config.view );
return ( evt, data, conversionApi ) => {
const match = matcher.match( data.viewItem );
// If there is no match, this callback should not do anything.
if ( !match ) {
return;
}
const modelKey = config.model.key;
const modelValue = typeof config.model.value == 'function' ?
config.model.value( data.viewItem, conversionApi ) : config.model.value;
// Do not convert if attribute building function returned falsy value.
if ( modelValue === null ) {
return;
}
if ( onlyViewNameIsDefined( config.view, data.viewItem ) ) {
match.match.name = true;
} else {
// Do not test or consume `name` consumable.
delete match.match.name;
}
// Try to consume appropriate values from consumable values list.
if ( !conversionApi.consumable.test( data.viewItem, match.match ) ) {
return;
}
// Since we are converting to attribute we need an range on which we will set the attribute.
// If the range is not created yet, we will create it.
if ( !data.modelRange ) {
// Convert children and set conversion result as a current data.
data = Object.assign( data, conversionApi.convertChildren( data.viewItem, data.modelCursor ) );
}
// Set attribute on current `output`. `Schema` is checked inside this helper function.
const attributeWasSet = setAttributeOn( data.modelRange, { key: modelKey, value: modelValue }, shallow, conversionApi );
if ( attributeWasSet ) {
conversionApi.consumable.consume( data.viewItem, match.match );
}
};
}
// Helper function that checks if element name should be consumed in attribute converters.
//
// @param {Object} config Conversion view config.
// @returns {Boolean}
function onlyViewNameIsDefined( viewConfig, viewItem ) {
// https://github.com/ckeditor/ckeditor5-engine/issues/1786
const configToTest = typeof viewConfig == 'function' ? viewConfig( viewItem ) : viewConfig;
if ( typeof configToTest == 'object' && !getViewElementNameFromConfig( configToTest ) ) {
return false;
}
return !configToTest.classes && !configToTest.attributes && !configToTest.styles;
}
// Helper function for to-model-attribute converter. Sets model attribute on given range. Checks {@link module:engine/model/schema~Schema}
// to ensure proper model structure.
//
// @param {module:engine/model/range~Range} modelRange Model range on which attribute should be set.
// @param {Object} modelAttribute Model attribute to set.
// @param {module:engine/conversion/upcastdispatcher~UpcastConversionApi} conversionApi Conversion API.
// @param {Boolean} shallow If set to `true` the attribute will be set only on top-level nodes. Otherwise, it will be set
// on all elements in the range.
// @returns {Boolean} `true` if attribute was set on at least one node from given `modelRange`.
function setAttributeOn( modelRange, modelAttribute, shallow, conversionApi ) {
let result = false;
// Set attribute on each item in range according to Schema.
for ( const node of Array.from( modelRange.getItems( { shallow } ) ) ) {
if ( conversionApi.schema.checkAttribute( node, modelAttribute.key ) ) {
conversionApi.writer.setAttribute( modelAttribute.key, modelAttribute.value, node );
result = true;
}
}
return result;
}
// Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastElementToMarker()`
// function and converts it to a format that is supported by `upcastElementToElement()` function.
//
// @param {Object} config Conversion configuration.
function normalizeElementToMarkerConfig( config ) {
const oldModel = config.model;
config.model = ( viewElement, conversionApi ) => {
const markerName = typeof oldModel == 'string' ? oldModel : oldModel( viewElement, conversionApi );
return conversionApi.writer.createElement( '$marker', { 'data-name': markerName } );
};
}
// Helper function for upcasting-to-marker conversion. Takes the config in a format requested by `upcastDataToMarker()`
// function and converts it to a format that is supported by `upcastElementToElement()` function.
//
// @param {Object} config Conversion configuration.
function normalizeDataToMarkerConfig( config, type ) {
const configForElements = {};
// Upcast and elements.
configForElements.view = config.view + '-' + type;
configForElements.model = ( viewElement, conversionApi ) => {
const viewName = viewElement.getAttribute( 'name' );
const markerName = config.model( viewName, conversionApi );
return conversionApi.writer.createElement( '$marker', { 'data-name': markerName } );
};
return configForElements;
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/conversion/viewconsumable.js":
/*!**********************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/conversion/viewconsumable.js ***!
\**********************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return ViewConsumable; });
/* harmony import */ var lodash_es__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! lodash-es */ "./node_modules/lodash-es/lodash.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/conversion/viewconsumable
*/
/**
* Class used for handling consumption of view {@link module:engine/view/element~Element elements},
* {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
* Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
* does not consume its attributes, classes and styles.
* To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
* To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
* To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
* To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
*
* viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
* viewConsumable.add( textNode ); // Adds text node for consumption.
* viewConsumable.add( docFragment ); // Adds document fragment for consumption.
* viewConsumable.test( element, { name: true } ); // Tests if element's name can be consumed.
* viewConsumable.test( textNode ); // Tests if text node can be consumed.
* viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
* viewConsumable.consume( element, { name: true } ); // Consume element's name.
* viewConsumable.consume( textNode ); // Consume text node.
* viewConsumable.consume( docFragment ); // Consume document fragment.
* viewConsumable.revert( element, { name: true } ); // Revert already consumed element's name.
* viewConsumable.revert( textNode ); // Revert already consumed text node.
* viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
*/
class ViewConsumable {
/**
* Creates new ViewConsumable.
*/
constructor() {
/**
* Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
* {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
* For {@link module:engine/view/text~Text text nodes} and
* {@link module:engine/view/documentfragment~DocumentFragment document fragments} boolean value is stored as value.
*
* @protected
* @member {Map.}
*/
this._consumables = new Map();
}
/**
* Adds {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
*
* viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
* viewConsumable.add( p, { attributes: 'name' } ); // Adds element's attribute.
* viewConsumable.add( p, { classes: 'foobar' } ); // Adds element's class.
* viewConsumable.add( p, { styles: 'color' } ); // Adds element's style
* viewConsumable.add( p, { attributes: 'name', styles: 'color' } ); // Adds attribute and style.
* viewConsumable.add( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
* viewConsumable.add( textNode ); // Adds text node to consume.
* viewConsumable.add( docFragment ); // Adds document fragment to consume.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
* attribute is provided - it should be handled separately by providing actual style/class.
*
* viewConsumable.add( p, { attributes: 'style' } ); // This call will throw an exception.
* viewConsumable.add( p, { styles: 'color' } ); // This is properly handled style.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names.
* @param {String|Array.} consumables.classes Class name or array of class names.
* @param {String|Array.} consumables.styles Style name or array of style names.
*/
add( element, consumables ) {
let elementConsumables;
// For text nodes and document fragments just mark them as consumable.
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
this._consumables.set( element, true );
return;
}
// For elements create new ViewElementConsumables or update already existing one.
if ( !this._consumables.has( element ) ) {
elementConsumables = new ViewElementConsumables( element );
this._consumables.set( element, elementConsumables );
} else {
elementConsumables = this._consumables.get( element );
}
elementConsumables.add( consumables );
}
/**
* Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
* It returns `true` when all items included in method's call can be consumed. Returns `false` when
* first already consumed item is found and `null` when first non-consumable item is found.
*
* viewConsumable.test( p, { name: true } ); // Tests element's name.
* viewConsumable.test( p, { attributes: 'name' } ); // Tests attribute.
* viewConsumable.test( p, { classes: 'foobar' } ); // Tests class.
* viewConsumable.test( p, { styles: 'color' } ); // Tests style.
* viewConsumable.test( p, { attributes: 'name', styles: 'color' } ); // Tests attribute and style.
* viewConsumable.test( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
* viewConsumable.test( textNode ); // Tests text node.
* viewConsumable.test( docFragment ); // Tests document fragment.
*
* Testing classes and styles as attribute will test if all added classes/styles can be consumed.
*
* viewConsumable.test( p, { attributes: 'class' } ); // Tests if all added classes can be consumed.
* viewConsumable.test( p, { attributes: 'style' } ); // Tests if all added styles can be consumed.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names.
* @param {String|Array.} consumables.classes Class name or array of class names.
* @param {String|Array.} consumables.styles Style name or array of style names.
* @returns {Boolean|null} Returns `true` when all items included in method's call can be consumed. Returns `false`
* when first already consumed item is found and `null` when first non-consumable item is found.
*/
test( element, consumables ) {
const elementConsumables = this._consumables.get( element );
if ( elementConsumables === undefined ) {
return null;
}
// For text nodes and document fragments return stored boolean value.
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
return elementConsumables;
}
// For elements test consumables object.
return elementConsumables.test( consumables );
}
/**
* Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
* It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
*
* viewConsumable.consume( p, { name: true } ); // Consumes element's name.
* viewConsumable.consume( p, { attributes: 'name' } ); // Consumes element's attribute.
* viewConsumable.consume( p, { classes: 'foobar' } ); // Consumes element's class.
* viewConsumable.consume( p, { styles: 'color' } ); // Consumes element's style.
* viewConsumable.consume( p, { attributes: 'name', styles: 'color' } ); // Consumes attribute and style.
* viewConsumable.consume( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
* viewConsumable.consume( textNode ); // Consumes text node.
* viewConsumable.consume( docFragment ); // Consumes document fragment.
*
* Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
*
* viewConsumable.consume( p, { attributes: 'class' } ); // Consume only if all added classes can be consumed.
* viewConsumable.consume( p, { attributes: 'style' } ); // Consume only if all added styles can be consumed.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names.
* @param {String|Array.} consumables.classes Class name or array of class names.
* @param {String|Array.} consumables.styles Style name or array of style names.
* @returns {Boolean} Returns `true` when all items included in method's call can be consumed,
* otherwise returns `false`.
*/
consume( element, consumables ) {
if ( this.test( element, consumables ) ) {
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
// For text nodes and document fragments set value to false.
this._consumables.set( element, false );
} else {
// For elements - consume consumables object.
this._consumables.get( element ).consume( consumables );
}
return true;
}
return false;
}
/**
* Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
* {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
* Method does not revert items that were never previously added for consumption, even if they are included in
* method's call.
*
* viewConsumable.revert( p, { name: true } ); // Reverts element's name.
* viewConsumable.revert( p, { attributes: 'name' } ); // Reverts element's attribute.
* viewConsumable.revert( p, { classes: 'foobar' } ); // Reverts element's class.
* viewConsumable.revert( p, { styles: 'color' } ); // Reverts element's style.
* viewConsumable.revert( p, { attributes: 'name', styles: 'color' } ); // Reverts attribute and style.
* viewConsumable.revert( p, { classes: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
* viewConsumable.revert( textNode ); // Reverts text node.
* viewConsumable.revert( docFragment ); // Reverts document fragment.
*
* Reverting classes and styles as attribute will revert all classes/styles that were previously added for
* consumption.
*
* viewConsumable.revert( p, { attributes: 'class' } ); // Reverts all classes added for consumption.
* viewConsumable.revert( p, { attributes: 'style' } ); // Reverts all styles added for consumption.
*
* @param {module:engine/view/element~Element|module:engine/view/text~Text|module:engine/view/documentfragment~DocumentFragment} element
* @param {Object} [consumables] Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
* @param {Boolean} consumables.name If set to true element's name will be included.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names.
* @param {String|Array.} consumables.classes Class name or array of class names.
* @param {String|Array.} consumables.styles Style name or array of style names.
*/
revert( element, consumables ) {
const elementConsumables = this._consumables.get( element );
if ( elementConsumables !== undefined ) {
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
// For text nodes and document fragments - set consumable to true.
this._consumables.set( element, true );
} else {
// For elements - revert items from consumables object.
elementConsumables.revert( consumables );
}
}
}
/**
* Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
* element's name and all its attributes, classes and styles.
*
* @static
* @param {module:engine/view/element~Element} element
* @returns {Object} consumables
*/
static consumablesFromElement( element ) {
const consumables = {
element,
name: true,
attributes: [],
classes: [],
styles: []
};
const attributes = element.getAttributeKeys();
for ( const attribute of attributes ) {
// Skip classes and styles - will be added separately.
if ( attribute == 'style' || attribute == 'class' ) {
continue;
}
consumables.attributes.push( attribute );
}
const classes = element.getClassNames();
for ( const className of classes ) {
consumables.classes.push( className );
}
const styles = element.getStyleNames();
for ( const style of styles ) {
consumables.styles.push( style );
}
return consumables;
}
/**
* Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
* {@link module:engine/view/node~Node node} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
* Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
*
* @static
* @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} from View node or document fragment
* from which `ViewConsumable` will be created.
* @param {module:engine/conversion/viewconsumable~ViewConsumable} [instance] If provided, given `ViewConsumable` instance will be used
* to add all consumables. It will be returned instead of a new instance.
*/
static createFrom( from, instance ) {
if ( !instance ) {
instance = new ViewConsumable( from );
}
if ( from.is( '$text' ) ) {
instance.add( from );
return instance;
}
// Add `from` itself, if it is an element.
if ( from.is( 'element' ) ) {
instance.add( from, ViewConsumable.consumablesFromElement( from ) );
}
if ( from.is( 'documentFragment' ) ) {
instance.add( from );
}
for ( const child of from.getChildren() ) {
instance = ViewConsumable.createFrom( child, instance );
}
return instance;
}
}
/**
* This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
* It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
*
* @private
*/
class ViewElementConsumables {
/**
* Creates ViewElementConsumables instance.
*
* @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} from View node or document fragment
* from which `ViewElementConsumables` is being created.
*/
constructor( from ) {
/**
* @readonly
* @member {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
*/
this.element = from;
/**
* Flag indicating if name of the element can be consumed.
*
* @private
* @member {Boolean}
*/
this._canConsumeName = null;
/**
* Contains maps of element's consumables: attributes, classes and styles.
*
* @private
* @member {Object}
*/
this._consumables = {
attributes: new Map(),
styles: new Map(),
classes: new Map()
};
}
/**
* Adds consumable parts of the {@link module:engine/view/element~Element view element}.
* Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
* styles still could be consumed):
*
* consumables.add( { name: true } );
*
* Attributes classes and styles:
*
* consumables.add( { attributes: 'title', classes: 'foo', styles: 'color' } );
* consumables.add( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
* attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
*
* @param {Object} consumables Object describing which parts of the element can be consumed.
* @param {Boolean} consumables.name If set to `true` element's name will be added as consumable.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names to add as consumable.
* @param {String|Array.} consumables.classes Class name or array of class names to add as consumable.
* @param {String|Array.} consumables.styles Style name or array of style names to add as consumable.
*/
add( consumables ) {
if ( consumables.name ) {
this._canConsumeName = true;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
this._add( type, consumables[ type ] );
}
}
}
/**
* Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed.
*
* Element's name can be tested:
*
* consumables.test( { name: true } );
*
* Attributes classes and styles:
*
* consumables.test( { attributes: 'title', classes: 'foo', styles: 'color' } );
* consumables.test( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
*
* @param {Object} consumables Object describing which parts of the element should be tested.
* @param {Boolean} consumables.name If set to `true` element's name will be tested.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names to test.
* @param {String|Array.} consumables.classes Class name or array of class names to test.
* @param {String|Array.} consumables.styles Style name or array of style names to test.
* @returns {Boolean|null} `true` when all tested items can be consumed, `null` when even one of the items
* was never marked for consumption and `false` when even one of the items was already consumed.
*/
test( consumables ) {
// Check if name can be consumed.
if ( consumables.name && !this._canConsumeName ) {
return this._canConsumeName;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
const value = this._test( type, consumables[ type ] );
if ( value !== true ) {
return value;
}
}
}
// Return true only if all can be consumed.
return true;
}
/**
* Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
* is already consumed - it consumes all consumable items provided.
* Element's name can be consumed:
*
* consumables.consume( { name: true } );
*
* Attributes classes and styles:
*
* consumables.consume( { attributes: 'title', classes: 'foo', styles: 'color' } );
* consumables.consume( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
*
* @param {Object} consumables Object describing which parts of the element should be consumed.
* @param {Boolean} consumables.name If set to `true` element's name will be consumed.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names to consume.
* @param {String|Array.} consumables.classes Class name or array of class names to consume.
* @param {String|Array.} consumables.styles Style name or array of style names to consume.
*/
consume( consumables ) {
if ( consumables.name ) {
this._canConsumeName = false;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
this._consume( type, consumables[ type ] );
}
}
}
/**
* Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
* Element's name can be reverted:
*
* consumables.revert( { name: true } );
*
* Attributes classes and styles:
*
* consumables.revert( { attributes: 'title', classes: 'foo', styles: 'color' } );
* consumables.revert( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
*
* @param {Object} consumables Object describing which parts of the element should be reverted.
* @param {Boolean} consumables.name If set to `true` element's name will be reverted.
* @param {String|Array.} consumables.attributes Attribute name or array of attribute names to revert.
* @param {String|Array.} consumables.classes Class name or array of class names to revert.
* @param {String|Array.} consumables.styles Style name or array of style names to revert.
*/
revert( consumables ) {
if ( consumables.name ) {
this._canConsumeName = true;
}
for ( const type in this._consumables ) {
if ( type in consumables ) {
this._revert( type, consumables[ type ] );
}
}
}
/**
* Helper method that adds consumables of a given type: attribute, class or style.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
* type is provided - it should be handled separately by providing actual style/class type.
*
* @private
* @param {String} type Type of the consumable item: `attributes`, `classes` or `styles`.
* @param {String|Array.} item Consumable item or array of items.
*/
_add( type, item ) {
const items = Object(lodash_es__WEBPACK_IMPORTED_MODULE_0__["isArray"])( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attributes' && ( name === 'class' || name === 'style' ) ) {
/**
* Class and style attributes should be handled separately in
* {@link module:engine/conversion/viewconsumable~ViewConsumable#add `ViewConsumable#add()`}.
*
* What you have done is trying to use:
*
* consumables.add( { attributes: [ 'class', 'style' ] } );
*
* While each class and style should be registered separately:
*
* consumables.add( { classes: 'some-class', styles: 'font-weight' } );
*
* @error viewconsumable-invalid-attribute
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"]( 'viewconsumable-invalid-attribute', this );
}
consumables.set( name, true );
if ( type === 'styles' ) {
for ( const alsoName of this.element.document.stylesProcessor.getRelatedStyles( name ) ) {
consumables.set( alsoName, true );
}
}
}
}
/**
* Helper method that tests consumables of a given type: attribute, class or style.
*
* @private
* @param {String} type Type of the consumable item: `attributes`, `classes` or `styles`.
* @param {String|Array.} item Consumable item or array of items.
* @returns {Boolean|null} Returns `true` if all items can be consumed, `null` when one of the items cannot be
* consumed and `false` when one of the items is already consumed.
*/
_test( type, item ) {
const items = Object(lodash_es__WEBPACK_IMPORTED_MODULE_0__["isArray"])( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attributes' && ( name === 'class' || name === 'style' ) ) {
const consumableName = name == 'class' ? 'classes' : 'styles';
// Check all classes/styles if class/style attribute is tested.
const value = this._test( consumableName, [ ...this._consumables[ consumableName ].keys() ] );
if ( value !== true ) {
return value;
}
} else {
const value = consumables.get( name );
// Return null if attribute is not found.
if ( value === undefined ) {
return null;
}
if ( !value ) {
return false;
}
}
}
return true;
}
/**
* Helper method that consumes items of a given type: attribute, class or style.
*
* @private
* @param {String} type Type of the consumable item: `attributes`, `classes` or `styles`.
* @param {String|Array.} item Consumable item or array of items.
*/
_consume( type, item ) {
const items = Object(lodash_es__WEBPACK_IMPORTED_MODULE_0__["isArray"])( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attributes' && ( name === 'class' || name === 'style' ) ) {
const consumableName = name == 'class' ? 'classes' : 'styles';
// If class or style is provided for consumption - consume them all.
this._consume( consumableName, [ ...this._consumables[ consumableName ].keys() ] );
} else {
consumables.set( name, false );
if ( type == 'styles' ) {
for ( const toConsume of this.element.document.stylesProcessor.getRelatedStyles( name ) ) {
consumables.set( toConsume, false );
}
}
}
}
}
/**
* Helper method that reverts items of a given type: attribute, class or style.
*
* @private
* @param {String} type Type of the consumable item: `attributes`, `classes` or , `styles`.
* @param {String|Array.} item Consumable item or array of items.
*/
_revert( type, item ) {
const items = Object(lodash_es__WEBPACK_IMPORTED_MODULE_0__["isArray"])( item ) ? item : [ item ];
const consumables = this._consumables[ type ];
for ( const name of items ) {
if ( type === 'attributes' && ( name === 'class' || name === 'style' ) ) {
const consumableName = name == 'class' ? 'classes' : 'styles';
// If class or style is provided for reverting - revert them all.
this._revert( consumableName, [ ...this._consumables[ consumableName ].keys() ] );
} else {
const value = consumables.get( name );
if ( value === false ) {
consumables.set( name, true );
}
}
}
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js":
/*!**************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js ***!
\**************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return BasicHtmlWriter; });
/**
* @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 engine/dataprocessor/basichtmlwriter
*/
/* globals document */
/**
* Basic HTML writer. It uses the native `innerHTML` property for basic conversion
* from a document fragment to an HTML string.
*
* @implements module:engine/dataprocessor/htmlwriter~HtmlWriter
*/
class BasicHtmlWriter {
/**
* Returns an HTML string created from the document fragment.
*
* @param {DocumentFragment} fragment
* @returns {String}
*/
getHtml( fragment ) {
const doc = document.implementation.createHTMLDocument( '' );
const container = doc.createElement( 'div' );
container.appendChild( fragment );
return container.innerHTML;
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js":
/*!****************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/dataprocessor/htmldataprocessor.js ***!
\****************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return HtmlDataProcessor; });
/* harmony import */ var _basichtmlwriter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./basichtmlwriter */ "./node_modules/@ckeditor/ckeditor5-engine/src/dataprocessor/basichtmlwriter.js");
/* harmony import */ var _view_domconverter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../view/domconverter */ "./node_modules/@ckeditor/ckeditor5-engine/src/view/domconverter.js");
/**
* @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 engine/dataprocessor/htmldataprocessor
*/
/* globals document, DOMParser */
/**
* The HTML data processor class.
* This data processor implementation uses HTML as input and output data.
*
* @implements module:engine/dataprocessor/dataprocessor~DataProcessor
*/
class HtmlDataProcessor {
/**
* Creates a new instance of the HTML data processor class.
*
* @param {module:engine/view/document~Document} document The view document instance.
*/
constructor( document ) {
/**
* A DOM parser instance used to parse an HTML string to an HTML document.
*
* @private
* @member {DOMParser}
*/
this._domParser = new DOMParser();
/**
* A DOM converter used to convert DOM elements to view elements.
*
* @private
* @member {module:engine/view/domconverter~DomConverter}
*/
this._domConverter = new _view_domconverter__WEBPACK_IMPORTED_MODULE_1__["default"]( document, { blockFillerMode: 'nbsp' } );
/**
* A basic HTML writer instance used to convert DOM elements to an HTML string.
*
* @private
* @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter}
*/
this._htmlWriter = new _basichtmlwriter__WEBPACK_IMPORTED_MODULE_0__["default"]();
}
/**
* Converts a provided {@link module:engine/view/documentfragment~DocumentFragment document fragment}
* to data format — in this case to an HTML string.
*
* @param {module:engine/view/documentfragment~DocumentFragment} viewFragment
* @returns {String} HTML string.
*/
toData( viewFragment ) {
// Convert view DocumentFragment to DOM DocumentFragment.
const domFragment = this._domConverter.viewToDom( viewFragment, document );
// Convert DOM DocumentFragment to HTML output.
return this._htmlWriter.getHtml( domFragment );
}
/**
* Converts the provided HTML string to a view tree.
*
* @param {String} data An HTML string.
* @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|null} A converted view element.
*/
toView( data ) {
// Convert input HTML data to DOM DocumentFragment.
const domFragment = this._toDom( data );
// Convert DOM DocumentFragment to view DocumentFragment.
return this._domConverter.domToView( domFragment );
}
/**
* Converts an HTML string to its DOM representation. Returns a document fragment containing nodes parsed from
* the provided data.
*
* @private
* @param {String} data
* @returns {DocumentFragment}
*/
_toDom( data ) {
const document = this._domParser.parseFromString( data, 'text/html' );
const fragment = document.createDocumentFragment();
const nodes = document.body.childNodes;
while ( nodes.length > 0 ) {
fragment.appendChild( nodes[ 0 ] );
}
return fragment;
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/batch.js":
/*!********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/batch.js ***!
\********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Batch; });
/**
* @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 engine/model/batch
*/
/**
* A batch instance groups model changes ({@link module:engine/model/operation/operation~Operation operations}). All operations
* grouped in a single batch can be reverted together, so you can also think about a batch as of a single undo step. If you want
* to extend a given undo step, you can add more changes to the batch using {@link module:engine/model/model~Model#enqueueChange}:
*
* model.enqueueChange( batch, writer => {
* writer.insertText( 'foo', paragraph, 'end' );
* } );
*
* @see module:engine/model/model~Model#enqueueChange
* @see module:engine/model/model~Model#change
*/
class Batch {
/**
* Creates a batch instance.
*
* @see module:engine/model/model~Model#enqueueChange
* @see module:engine/model/model~Model#change
* @param {'transparent'|'default'} [type='default'] The type of the batch.
*/
constructor( type = 'default' ) {
/**
* An array of operations that compose this batch.
*
* @readonly
* @type {Array.}
*/
this.operations = [];
/**
* The type of the batch.
*
* It can be one of the following values:
* * `'default'` – All "normal" batches. This is the most commonly used type.
* * `'transparent'` – A batch that should be ignored by other features, i.e. an initial batch or collaborative editing
* changes.
*
* @readonly
* @type {'transparent'|'default'}
*/
this.type = type;
}
/**
* Returns the base version of this batch, which is equal to the base version of the first operation in the batch.
* If there are no operations in the batch or neither operation has the base version set, it returns `null`.
*
* @readonly
* @type {Number|null}
*/
get baseVersion() {
for ( const op of this.operations ) {
if ( op.baseVersion !== null ) {
return op.baseVersion;
}
}
return null;
}
/**
* Adds an operation to the batch instance.
*
* @param {module:engine/model/operation/operation~Operation} operation An operation to add.
* @returns {module:engine/model/operation/operation~Operation} The added operation.
*/
addOperation( operation ) {
operation.batch = this;
this.operations.push( operation );
return operation;
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/differ.js":
/*!*********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/differ.js ***!
\*********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Differ; });
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/**
* @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 engine/model/differ
*/
/**
* Calculates the difference between two model states.
*
* Receives operations that are to be applied on the model document. Marks parts of the model document tree which
* are changed and saves the state of these elements before the change. Then, it compares saved elements with the
* changed elements, after all changes are applied on the model document. Calculates the diff between saved
* elements and new ones and returns a change set.
*/
class Differ {
/**
* Creates a `Differ` instance.
*
* @param {module:engine/model/markercollection~MarkerCollection} markerCollection Model's marker collection.
*/
constructor( markerCollection ) {
/**
* Reference to the model's marker collection.
*
* @private
* @type {module:engine/model/markercollection~MarkerCollection}
*/
this._markerCollection = markerCollection;
/**
* A map that stores changes that happened in a given element.
*
* The keys of the map are references to the model elements.
* The values of the map are arrays with changes that were done on this element.
*
* @private
* @type {Map}
*/
this._changesInElement = new Map();
/**
* A map that stores "element's children snapshots". A snapshot is representing children of a given element before
* the first change was applied on that element. Snapshot items are objects with two properties: `name`,
* containing the element name (or `'$text'` for a text node) and `attributes` which is a map of the node's attributes.
*
* @private
* @type {Map}
*/
this._elementSnapshots = new Map();
/**
* A map that stores all changed markers.
*
* The keys of the map are marker names.
* The values of the map are objects with the `oldRange` and `newRange` properties. They store the marker range
* state before and after the change.
*
* @private
* @type {Map}
*/
this._changedMarkers = new Map();
/**
* Stores the number of changes that were processed. Used to order the changes chronologically. It is important
* when changes are sorted.
*
* @private
* @type {Number}
*/
this._changeCount = 0;
/**
* For efficiency purposes, `Differ` stores the change set returned by the differ after {@link #getChanges} call.
* Cache is reset each time a new operation is buffered. If the cache has not been reset, {@link #getChanges} will
* return the cached value instead of calculating it again.
*
* This property stores those changes that did not take place in graveyard root.
*
* @private
* @type {Array.|null}
*/
this._cachedChanges = null;
/**
* For efficiency purposes, `Differ` stores the change set returned by the differ after the {@link #getChanges} call.
* The cache is reset each time a new operation is buffered. If the cache has not been reset, {@link #getChanges} will
* return the cached value instead of calculating it again.
*
* This property stores all changes evaluated by `Differ`, including those that took place in the graveyard.
*
* @private
* @type {Array.|null}
*/
this._cachedChangesWithGraveyard = null;
}
/**
* Informs whether there are any changes buffered in `Differ`.
*
* @readonly
* @type {Boolean}
*/
get isEmpty() {
return this._changesInElement.size == 0 && this._changedMarkers.size == 0;
}
/**
* Marks given `item` in differ to be "refreshed". It means that the item will be marked as removed and inserted in the differ changes
* set, so it will be effectively re-converted when differ changes will be handled by a dispatcher.
*
* @param {module:engine/model/item~Item} item Item to refresh.
*/
refreshItem( item ) {
if ( this._isInInsertedElement( item.parent ) ) {
return;
}
this._markRemove( item.parent, item.startOffset, item.offsetSize );
this._markInsert( item.parent, item.startOffset, item.offsetSize );
const range = _range__WEBPACK_IMPORTED_MODULE_1__["default"]._createOn( item );
for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
const markerRange = marker.getRange();
this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
}
// Clear cache after each buffered operation as it is no longer valid.
this._cachedChanges = null;
}
/**
* Buffers the given operation. An operation has to be buffered before it is executed.
*
* Operation type is checked and it is checked which nodes it will affect. These nodes are then stored in `Differ`
* in the state before the operation is executed.
*
* @param {module:engine/model/operation/operation~Operation} operation An operation to buffer.
*/
bufferOperation( operation ) {
// Below we take an operation, check its type, then use its parameters in marking (private) methods.
// The general rule is to not mark elements inside inserted element. All inserted elements are re-rendered.
// Marking changes in them would cause a "double" changing then.
//
switch ( operation.type ) {
case 'insert': {
if ( this._isInInsertedElement( operation.position.parent ) ) {
return;
}
this._markInsert( operation.position.parent, operation.position.offset, operation.nodes.maxOffset );
break;
}
case 'addAttribute':
case 'removeAttribute':
case 'changeAttribute': {
for ( const item of operation.range.getItems( { shallow: true } ) ) {
if ( this._isInInsertedElement( item.parent ) ) {
continue;
}
this._markAttribute( item );
}
break;
}
case 'remove':
case 'move':
case 'reinsert': {
// When range is moved to the same position then not mark it as a change.
// See: https://github.com/ckeditor/ckeditor5-engine/issues/1664.
if (
operation.sourcePosition.isEqual( operation.targetPosition ) ||
operation.sourcePosition.getShiftedBy( operation.howMany ).isEqual( operation.targetPosition )
) {
return;
}
const sourceParentInserted = this._isInInsertedElement( operation.sourcePosition.parent );
const targetParentInserted = this._isInInsertedElement( operation.targetPosition.parent );
if ( !sourceParentInserted ) {
this._markRemove( operation.sourcePosition.parent, operation.sourcePosition.offset, operation.howMany );
}
if ( !targetParentInserted ) {
this._markInsert( operation.targetPosition.parent, operation.getMovedRangeStart().offset, operation.howMany );
}
break;
}
case 'rename': {
if ( this._isInInsertedElement( operation.position.parent ) ) {
return;
}
this._markRemove( operation.position.parent, operation.position.offset, 1 );
this._markInsert( operation.position.parent, operation.position.offset, 1 );
const range = _range__WEBPACK_IMPORTED_MODULE_1__["default"]._createFromPositionAndShift( operation.position, 1 );
for ( const marker of this._markerCollection.getMarkersIntersectingRange( range ) ) {
const markerRange = marker.getRange();
this.bufferMarkerChange( marker.name, markerRange, markerRange, marker.affectsData );
}
break;
}
case 'split': {
const splitElement = operation.splitPosition.parent;
// Mark that children of the split element were removed.
if ( !this._isInInsertedElement( splitElement ) ) {
this._markRemove( splitElement, operation.splitPosition.offset, operation.howMany );
}
// Mark that the new element (split copy) was inserted.
if ( !this._isInInsertedElement( operation.insertionPosition.parent ) ) {
this._markInsert( operation.insertionPosition.parent, operation.insertionPosition.offset, 1 );
}
// If the split took the element from the graveyard, mark that the element from the graveyard was removed.
if ( operation.graveyardPosition ) {
this._markRemove( operation.graveyardPosition.parent, operation.graveyardPosition.offset, 1 );
}
break;
}
case 'merge': {
// Mark that the merged element was removed.
const mergedElement = operation.sourcePosition.parent;
if ( !this._isInInsertedElement( mergedElement.parent ) ) {
this._markRemove( mergedElement.parent, mergedElement.startOffset, 1 );
}
// Mark that the merged element was inserted into graveyard.
const graveyardParent = operation.graveyardPosition.parent;
this._markInsert( graveyardParent, operation.graveyardPosition.offset, 1 );
// Mark that children of merged element were inserted at new parent.
const mergedIntoElement = operation.targetPosition.parent;
if ( !this._isInInsertedElement( mergedIntoElement ) ) {
this._markInsert( mergedIntoElement, operation.targetPosition.offset, mergedElement.maxOffset );
}
break;
}
}
// Clear cache after each buffered operation as it is no longer valid.
this._cachedChanges = null;
}
/**
* Buffers a marker change.
*
* @param {String} markerName The name of the marker that changed.
* @param {module:engine/model/range~Range|null} oldRange Marker range before the change or `null` if the marker has just
* been created.
* @param {module:engine/model/range~Range|null} newRange Marker range after the change or `null` if the marker was removed.
* @param {Boolean} affectsData Flag indicating whether marker affects the editor data.
*/
bufferMarkerChange( markerName, oldRange, newRange, affectsData ) {
const buffered = this._changedMarkers.get( markerName );
if ( !buffered ) {
this._changedMarkers.set( markerName, {
oldRange,
newRange,
affectsData
} );
} else {
buffered.newRange = newRange;
buffered.affectsData = affectsData;
if ( buffered.oldRange == null && buffered.newRange == null ) {
// The marker is going to be removed (`newRange == null`) but it did not exist before the first buffered change
// (`buffered.oldRange == null`). In this case, do not keep the marker in buffer at all.
this._changedMarkers.delete( markerName );
}
}
}
/**
* Returns all markers that should be removed as a result of buffered changes.
*
* @returns {Array.} Markers to remove. Each array item is an object containing the `name` and `range` properties.
*/
getMarkersToRemove() {
const result = [];
for ( const [ name, change ] of this._changedMarkers ) {
if ( change.oldRange != null ) {
result.push( { name, range: change.oldRange } );
}
}
return result;
}
/**
* Returns all markers which should be added as a result of buffered changes.
*
* @returns {Array.} Markers to add. Each array item is an object containing the `name` and `range` properties.
*/
getMarkersToAdd() {
const result = [];
for ( const [ name, change ] of this._changedMarkers ) {
if ( change.newRange != null ) {
result.push( { name, range: change.newRange } );
}
}
return result;
}
/**
* Returns all markers which changed.
*
* @returns {Array.}
*/
getChangedMarkers() {
return Array.from( this._changedMarkers ).map( item => (
{
name: item[ 0 ],
data: {
oldRange: item[ 1 ].oldRange,
newRange: item[ 1 ].newRange
}
}
) );
}
/**
* Checks whether some of the buffered changes affect the editor data.
*
* Types of changes which affect the editor data:
*
* * model structure changes,
* * attribute changes,
* * changes of markers which were defined as `affectingData`.
*
* @returns {Boolean}
*/
hasDataChanges() {
for ( const [ , change ] of this._changedMarkers ) {
if ( change.affectsData ) {
return true;
}
}
// If markers do not affect the data, check whether there are some changes in elements.
return this._changesInElement.size > 0;
}
/**
* Calculates the diff between the old model tree state (the state before the first buffered operations since the last {@link #reset}
* call) and the new model tree state (actual one). It should be called after all buffered operations are executed.
*
* The diff set is returned as an array of diff items, each describing a change done on the model. The items are sorted by
* the position on which the change happened. If a position {@link module:engine/model/position~Position#isBefore is before}
* another one, it will be on an earlier index in the diff set.
*
* Because calculating the diff is a costly operation, the result is cached. If no new operation was buffered since the
* previous {@link #getChanges} call, the next call will return the cached value.
*
* @param {Object} options Additional options.
* @param {Boolean} [options.includeChangesInGraveyard=false] If set to `true`, also changes that happened
* in the graveyard root will be returned. By default, changes in the graveyard root are not returned.
* @returns {Array.} Diff between the old and the new model tree state.
*/
getChanges( options = { includeChangesInGraveyard: false } ) {
// If there are cached changes, just return them instead of calculating changes again.
if ( this._cachedChanges ) {
if ( options.includeChangesInGraveyard ) {
return this._cachedChangesWithGraveyard.slice();
} else {
return this._cachedChanges.slice();
}
}
// Will contain returned results.
const diffSet = [];
// Check all changed elements.
for ( const element of this._changesInElement.keys() ) {
// Get changes for this element and sort them.
const changes = this._changesInElement.get( element ).sort( ( a, b ) => {
if ( a.offset === b.offset ) {
if ( a.type != b.type ) {
// If there are multiple changes at the same position, "remove" change should be first.
// If the order is different, for example, we would first add some nodes and then removed them
// (instead of the nodes that we should remove).
return a.type == 'remove' ? -1 : 1;
}
return 0;
}
return a.offset < b.offset ? -1 : 1;
} );
// Get children of this element before any change was applied on it.
const snapshotChildren = this._elementSnapshots.get( element );
// Get snapshot of current element's children.
const elementChildren = _getChildrenSnapshot( element.getChildren() );
// Generate actions basing on changes done on element.
const actions = _generateActionsFromChanges( snapshotChildren.length, changes );
let i = 0; // Iterator in `elementChildren` array -- iterates through current children of element.
let j = 0; // Iterator in `snapshotChildren` array -- iterates through old children of element.
// Process every action.
for ( const action of actions ) {
if ( action === 'i' ) {
// Generate diff item for this element and insert it into the diff set.
diffSet.push( this._getInsertDiff( element, i, elementChildren[ i ].name ) );
i++;
} else if ( action === 'r' ) {
// Generate diff item for this element and insert it into the diff set.
diffSet.push( this._getRemoveDiff( element, i, snapshotChildren[ j ].name ) );
j++;
} else if ( action === 'a' ) {
// Take attributes from saved and current children.
const elementAttributes = elementChildren[ i ].attributes;
const snapshotAttributes = snapshotChildren[ j ].attributes;
let range;
if ( elementChildren[ i ].name == '$text' ) {
range = new _range__WEBPACK_IMPORTED_MODULE_1__["default"]( _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( element, i ), _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( element, i + 1 ) );
} else {
const index = element.offsetToIndex( i );
range = new _range__WEBPACK_IMPORTED_MODULE_1__["default"]( _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( element, i ), _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( element.getChild( index ), 0 ) );
}
// Generate diff items for this change (there might be multiple attributes changed and
// there is a single diff for each of them) and insert them into the diff set.
diffSet.push( ...this._getAttributesDiff( range, snapshotAttributes, elementAttributes ) );
i++;
j++;
} else {
// `action` is 'equal'. Child not changed.
i++;
j++;
}
}
}
// Then, sort the changes by the position (change at position before other changes is first).
diffSet.sort( ( a, b ) => {
// If the change is in different root, we don't care much, but we'd like to have all changes in given
// root "together" in the array. So let's just sort them by the root name. It does not matter which root
// will be processed first.
if ( a.position.root != b.position.root ) {
return a.position.root.rootName < b.position.root.rootName ? -1 : 1;
}
// If change happens at the same position...
if ( a.position.isEqual( b.position ) ) {
// Keep chronological order of operations.
return a.changeCount - b.changeCount;
}
// If positions differ, position "on the left" should be earlier in the result.
return a.position.isBefore( b.position ) ? -1 : 1;
} );
// Glue together multiple changes (mostly on text nodes).
for ( let i = 1; i < diffSet.length; i++ ) {
const prevDiff = diffSet[ i - 1 ];
const thisDiff = diffSet[ i ];
// Glue remove changes if they happen on text on same position.
const isConsecutiveTextRemove =
prevDiff.type == 'remove' && thisDiff.type == 'remove' &&
prevDiff.name == '$text' && thisDiff.name == '$text' &&
prevDiff.position.isEqual( thisDiff.position );
// Glue insert changes if they happen on text on consecutive fragments.
const isConsecutiveTextAdd =
prevDiff.type == 'insert' && thisDiff.type == 'insert' &&
prevDiff.name == '$text' && thisDiff.name == '$text' &&
prevDiff.position.parent == thisDiff.position.parent &&
prevDiff.position.offset + prevDiff.length == thisDiff.position.offset;
// Glue attribute changes if they happen on consecutive fragments and have same key, old value and new value.
const isConsecutiveAttributeChange =
prevDiff.type == 'attribute' && thisDiff.type == 'attribute' &&
prevDiff.position.parent == thisDiff.position.parent &&
prevDiff.range.isFlat && thisDiff.range.isFlat &&
prevDiff.position.offset + prevDiff.length == thisDiff.position.offset &&
prevDiff.attributeKey == thisDiff.attributeKey &&
prevDiff.attributeOldValue == thisDiff.attributeOldValue &&
prevDiff.attributeNewValue == thisDiff.attributeNewValue;
if ( isConsecutiveTextRemove || isConsecutiveTextAdd || isConsecutiveAttributeChange ) {
diffSet[ i - 1 ].length++;
if ( isConsecutiveAttributeChange ) {
diffSet[ i - 1 ].range.end = diffSet[ i - 1 ].range.end.getShiftedBy( 1 );
}
diffSet.splice( i, 1 );
i--;
}
}
// Remove `changeCount` property from diff items. It is used only for sorting and is internal thing.
for ( const item of diffSet ) {
delete item.changeCount;
if ( item.type == 'attribute' ) {
delete item.position;
delete item.length;
}
}
this._changeCount = 0;
// Cache changes.
this._cachedChangesWithGraveyard = diffSet.slice();
this._cachedChanges = diffSet.slice().filter( _changesInGraveyardFilter );
if ( options.includeChangesInGraveyard ) {
return this._cachedChangesWithGraveyard;
} else {
return this._cachedChanges;
}
}
/**
* Resets `Differ`. Removes all buffered changes.
*/
reset() {
this._changesInElement.clear();
this._elementSnapshots.clear();
this._changedMarkers.clear();
this._cachedChanges = null;
}
/**
* Saves and handles an insert change.
*
* @private
* @param {module:engine/model/element~Element} parent
* @param {Number} offset
* @param {Number} howMany
*/
_markInsert( parent, offset, howMany ) {
const changeItem = { type: 'insert', offset, howMany, count: this._changeCount++ };
this._markChange( parent, changeItem );
}
/**
* Saves and handles a remove change.
*
* @private
* @param {module:engine/model/element~Element} parent
* @param {Number} offset
* @param {Number} howMany
*/
_markRemove( parent, offset, howMany ) {
const changeItem = { type: 'remove', offset, howMany, count: this._changeCount++ };
this._markChange( parent, changeItem );
this._removeAllNestedChanges( parent, offset, howMany );
}
/**
* Saves and handles an attribute change.
*
* @private
* @param {module:engine/model/item~Item} item
*/
_markAttribute( item ) {
const changeItem = { type: 'attribute', offset: item.startOffset, howMany: item.offsetSize, count: this._changeCount++ };
this._markChange( item.parent, changeItem );
}
/**
* Saves and handles a model change.
*
* @private
* @param {module:engine/model/element~Element} parent
* @param {Object} changeItem
*/
_markChange( parent, changeItem ) {
// First, make a snapshot of this parent's children (it will be made only if it was not made before).
this._makeSnapshot( parent );
// Then, get all changes that already were done on the element (empty array if this is the first change).
const changes = this._getChangesForElement( parent );
// Then, look through all the changes, and transform them or the new change.
this._handleChange( changeItem, changes );
// Add the new change.
changes.push( changeItem );
// Remove incorrect changes. During transformation some change might be, for example, included in another.
// In that case, the change will have `howMany` property set to `0` or less. We need to remove those changes.
for ( let i = 0; i < changes.length; i++ ) {
if ( changes[ i ].howMany < 1 ) {
changes.splice( i, 1 );
i--;
}
}
}
/**
* Gets an array of changes that have already been saved for a given element.
*
* @private
* @param {module:engine/model/element~Element} element
* @returns {Array.}
*/
_getChangesForElement( element ) {
let changes;
if ( this._changesInElement.has( element ) ) {
changes = this._changesInElement.get( element );
} else {
changes = [];
this._changesInElement.set( element, changes );
}
return changes;
}
/**
* Saves a children snapshot for a given element.
*
* @private
* @param {module:engine/model/element~Element} element
*/
_makeSnapshot( element ) {
if ( !this._elementSnapshots.has( element ) ) {
this._elementSnapshots.set( element, _getChildrenSnapshot( element.getChildren() ) );
}
}
/**
* For a given newly saved change, compares it with a change already done on the element and modifies the incoming
* change and/or the old change.
*
* @private
* @param {Object} inc Incoming (new) change.
* @param {Array.} changes An array containing all the changes done on that element.
*/
_handleChange( inc, changes ) {
// We need a helper variable that will store how many nodes are to be still handled for this change item.
// `nodesToHandle` (how many nodes still need to be handled) and `howMany` (how many nodes were affected)
// needs to be differentiated.
//
// This comes up when there are multiple changes that are affected by `inc` change item.
//
// For example: assume two insert changes: `{ offset: 2, howMany: 1 }` and `{ offset: 5, howMany: 1 }`.
// Assume that `inc` change is remove `{ offset: 2, howMany: 2, nodesToHandle: 2 }`.
//
// Then, we:
// - "forget" about first insert change (it is "eaten" by remove),
// - because of that, at the end we will want to remove only one node (`nodesToHandle = 1`),
// - but still we have to change offset of the second insert change from `5` to `3`!
//
// So, `howMany` does not change throughout items transformation and keeps information about how many nodes were affected,
// while `nodesToHandle` means how many nodes need to be handled after the change item is transformed by other changes.
inc.nodesToHandle = inc.howMany;
for ( const old of changes ) {
const incEnd = inc.offset + inc.howMany;
const oldEnd = old.offset + old.howMany;
if ( inc.type == 'insert' ) {
if ( old.type == 'insert' ) {
if ( inc.offset <= old.offset ) {
old.offset += inc.howMany;
} else if ( inc.offset < oldEnd ) {
old.howMany += inc.nodesToHandle;
inc.nodesToHandle = 0;
}
}
if ( old.type == 'remove' ) {
if ( inc.offset < old.offset ) {
old.offset += inc.howMany;
}
}
if ( old.type == 'attribute' ) {
if ( inc.offset <= old.offset ) {
old.offset += inc.howMany;
} else if ( inc.offset < oldEnd ) {
// This case is more complicated, because attribute change has to be split into two.
// Example (assume that uppercase and lowercase letters mean different attributes):
//
// initial state: abcxyz
// attribute change: aBCXYz
// incoming insert: aBCfooXYz
//
// Change ranges cannot intersect because each item has to be described exactly (it was either
// not changed, inserted, removed, or its attribute was changed). That's why old attribute
// change has to be split and both parts has to be handled separately from now on.
const howMany = old.howMany;
old.howMany = inc.offset - old.offset;
// Add the second part of attribute change to the beginning of processed array so it won't
// be processed again in this loop.
changes.unshift( {
type: 'attribute',
offset: incEnd,
howMany: howMany - old.howMany,
count: this._changeCount++
} );
}
}
}
if ( inc.type == 'remove' ) {
if ( old.type == 'insert' ) {
if ( incEnd <= old.offset ) {
old.offset -= inc.howMany;
} else if ( incEnd <= oldEnd ) {
if ( inc.offset < old.offset ) {
const intersectionLength = incEnd - old.offset;
old.offset = inc.offset;
old.howMany -= intersectionLength;
inc.nodesToHandle -= intersectionLength;
} else {
old.howMany -= inc.nodesToHandle;
inc.nodesToHandle = 0;
}
} else {
if ( inc.offset <= old.offset ) {
inc.nodesToHandle -= old.howMany;
old.howMany = 0;
} else if ( inc.offset < oldEnd ) {
const intersectionLength = oldEnd - inc.offset;
old.howMany -= intersectionLength;
inc.nodesToHandle -= intersectionLength;
}
}
}
if ( old.type == 'remove' ) {
if ( incEnd <= old.offset ) {
old.offset -= inc.howMany;
} else if ( inc.offset < old.offset ) {
inc.nodesToHandle += old.howMany;
old.howMany = 0;
}
}
if ( old.type == 'attribute' ) {
if ( incEnd <= old.offset ) {
old.offset -= inc.howMany;
} else if ( inc.offset < old.offset ) {
const intersectionLength = incEnd - old.offset;
old.offset = inc.offset;
old.howMany -= intersectionLength;
} else if ( inc.offset < oldEnd ) {
if ( incEnd <= oldEnd ) {
// On first sight in this case we don't need to split attribute operation into two.
// However the changes set is later converted to actions (see `_generateActionsFromChanges`).
// For that reason, no two changes may intersect.
// So we cannot have an attribute change that "contains" remove change.
// Attribute change needs to be split.
const howMany = old.howMany;
old.howMany = inc.offset - old.offset;
const howManyAfter = howMany - old.howMany - inc.nodesToHandle;
// Add the second part of attribute change to the beginning of processed array so it won't
// be processed again in this loop.
changes.unshift( {
type: 'attribute',
offset: inc.offset,
howMany: howManyAfter,
count: this._changeCount++
} );
} else {
old.howMany -= oldEnd - inc.offset;
}
}
}
}
if ( inc.type == 'attribute' ) {
// In case of attribute change, `howMany` should be kept same as `nodesToHandle`. It's not an error.
if ( old.type == 'insert' ) {
if ( inc.offset < old.offset && incEnd > old.offset ) {
if ( incEnd > oldEnd ) {
// This case is similar to a case described when incoming change was insert and old change was attribute.
// See comment above.
//
// This time incoming change is attribute. We need to split incoming change in this case too.
// However this time, the second part of the attribute change needs to be processed further
// because there might be other changes that it collides with.
const attributePart = {
type: 'attribute',
offset: oldEnd,
howMany: incEnd - oldEnd,
count: this._changeCount++
};
this._handleChange( attributePart, changes );
changes.push( attributePart );
}
inc.nodesToHandle = old.offset - inc.offset;
inc.howMany = inc.nodesToHandle;
} else if ( inc.offset >= old.offset && inc.offset < oldEnd ) {
if ( incEnd > oldEnd ) {
inc.nodesToHandle = incEnd - oldEnd;
inc.offset = oldEnd;
} else {
inc.nodesToHandle = 0;
}
}
}
if ( old.type == 'remove' ) {
// This is a case when attribute change "contains" remove change.
// The attribute change needs to be split into two because changes cannot intersect.
if ( inc.offset < old.offset && incEnd > old.offset ) {
const attributePart = {
type: 'attribute',
offset: old.offset,
howMany: incEnd - old.offset,
count: this._changeCount++
};
this._handleChange( attributePart, changes );
changes.push( attributePart );
inc.nodesToHandle = old.offset - inc.offset;
inc.howMany = inc.nodesToHandle;
}
}
if ( old.type == 'attribute' ) {
// There are only two conflicting scenarios possible here:
if ( inc.offset >= old.offset && incEnd <= oldEnd ) {
// `old` change includes `inc` change, or they are the same.
inc.nodesToHandle = 0;
inc.howMany = 0;
inc.offset = 0;
} else if ( inc.offset <= old.offset && incEnd >= oldEnd ) {
// `inc` change includes `old` change.
old.howMany = 0;
}
}
}
}
inc.howMany = inc.nodesToHandle;
delete inc.nodesToHandle;
}
/**
* Returns an object with a single insert change description.
*
* @private
* @param {module:engine/model/element~Element} parent The element in which the change happened.
* @param {Number} offset The offset at which change happened.
* @param {String} name The name of the removed element or `'$text'` for a character.
* @returns {Object} The diff item.
*/
_getInsertDiff( parent, offset, name ) {
return {
type: 'insert',
position: _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( parent, offset ),
name,
length: 1,
changeCount: this._changeCount++
};
}
/**
* Returns an object with a single remove change description.
*
* @private
* @param {module:engine/model/element~Element} parent The element in which change happened.
* @param {Number} offset The offset at which change happened.
* @param {String} name The name of the removed element or `'$text'` for a character.
* @returns {Object} The diff item.
*/
_getRemoveDiff( parent, offset, name ) {
return {
type: 'remove',
position: _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( parent, offset ),
name,
length: 1,
changeCount: this._changeCount++
};
}
/**
* Returns an array of objects where each one is a single attribute change description.
*
* @private
* @param {module:engine/model/range~Range} range The range where the change happened.
* @param {Map} oldAttributes A map, map iterator or compatible object that contains attributes before the change.
* @param {Map} newAttributes A map, map iterator or compatible object that contains attributes after the change.
* @returns {Array.} An array containing one or more diff items.
*/
_getAttributesDiff( range, oldAttributes, newAttributes ) {
// Results holder.
const diffs = [];
// Clone new attributes as we will be performing changes on this object.
newAttributes = new Map( newAttributes );
// Look through old attributes.
for ( const [ key, oldValue ] of oldAttributes ) {
// Check what is the new value of the attribute (or if it was removed).
const newValue = newAttributes.has( key ) ? newAttributes.get( key ) : null;
// If values are different (or attribute was removed)...
if ( newValue !== oldValue ) {
// Add diff item.
diffs.push( {
type: 'attribute',
position: range.start,
range: range.clone(),
length: 1,
attributeKey: key,
attributeOldValue: oldValue,
attributeNewValue: newValue,
changeCount: this._changeCount++
} );
}
// Prevent returning two diff items for the same change.
newAttributes.delete( key );
}
// Look through new attributes that weren't handled above.
for ( const [ key, newValue ] of newAttributes ) {
// Each of them is a new attribute. Add diff item.
diffs.push( {
type: 'attribute',
position: range.start,
range: range.clone(),
length: 1,
attributeKey: key,
attributeOldValue: null,
attributeNewValue: newValue,
changeCount: this._changeCount++
} );
}
return diffs;
}
/**
* Checks whether given element or any of its parents is an element that is buffered as an inserted element.
*
* @private
* @param {module:engine/model/element~Element} element Element to check.
* @returns {Boolean}
*/
_isInInsertedElement( element ) {
const parent = element.parent;
if ( !parent ) {
return false;
}
const changes = this._changesInElement.get( parent );
const offset = element.startOffset;
if ( changes ) {
for ( const change of changes ) {
if ( change.type == 'insert' && offset >= change.offset && offset < change.offset + change.howMany ) {
return true;
}
}
}
return this._isInInsertedElement( parent );
}
/**
* Removes deeply all buffered changes that are registered in elements from range specified by `parent`, `offset`
* and `howMany`.
*
* @private
* @param {module:engine/model/element~Element} parent
* @param {Number} offset
* @param {Number} howMany
*/
_removeAllNestedChanges( parent, offset, howMany ) {
const range = new _range__WEBPACK_IMPORTED_MODULE_1__["default"]( _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( parent, offset ), _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( parent, offset + howMany ) );
for ( const item of range.getItems( { shallow: true } ) ) {
if ( item.is( 'element' ) ) {
this._elementSnapshots.delete( item );
this._changesInElement.delete( item );
this._removeAllNestedChanges( item, 0, item.maxOffset );
}
}
}
}
// Returns an array that is a copy of passed child list with the exception that text nodes are split to one or more
// objects, each representing one character and attributes set on that character.
function _getChildrenSnapshot( children ) {
const snapshot = [];
for ( const child of children ) {
if ( child.is( '$text' ) ) {
for ( let i = 0; i < child.data.length; i++ ) {
snapshot.push( {
name: '$text',
attributes: new Map( child.getAttributes() )
} );
}
} else {
snapshot.push( {
name: child.name,
attributes: new Map( child.getAttributes() )
} );
}
}
return snapshot;
}
// Generates array of actions for given changes set.
// It simulates what `diff` function does.
// Generated actions are:
// - 'e' for 'equal' - when item at that position did not change,
// - 'i' for 'insert' - when item at that position was inserted,
// - 'r' for 'remove' - when item at that position was removed,
// - 'a' for 'attribute' - when item at that position has it attributes changed.
//
// Example (assume that uppercase letters have bold attribute, compare with function code):
//
// children before: fooBAR
// children after: foxybAR
//
// changes: type: remove, offset: 1, howMany: 1
// type: insert, offset: 2, howMany: 2
// type: attribute, offset: 4, howMany: 1
//
// expected actions: equal (f), remove (o), equal (o), insert (x), insert (y), attribute (b), equal (A), equal (R)
//
// steps taken by th script:
//
// 1. change = "type: remove, offset: 1, howMany: 1"; offset = 0; oldChildrenHandled = 0
// 1.1 between this change and the beginning is one not-changed node, fill with one equal action, one old child has been handled
// 1.2 this change removes one node, add one remove action
// 1.3 change last visited `offset` to 1
// 1.4 since an old child has been removed, one more old child has been handled
// 1.5 actions at this point are: equal, remove
//
// 2. change = "type: insert, offset: 2, howMany: 2"; offset = 1; oldChildrenHandled = 2
// 2.1 between this change and previous change is one not-changed node, add equal action, another one old children has been handled
// 2.2 this change inserts two nodes, add two insert actions
// 2.3 change last visited offset to the end of the inserted range, that is 4
// 2.4 actions at this point are: equal, remove, equal, insert, insert
//
// 3. change = "type: attribute, offset: 4, howMany: 1"; offset = 4, oldChildrenHandled = 3
// 3.1 between this change and previous change are no not-changed nodes
// 3.2 this change changes one node, add one attribute action
// 3.3 change last visited `offset` to the end of change range, that is 5
// 3.4 since an old child has been changed, one more old child has been handled
// 3.5 actions at this point are: equal, remove, equal, insert, insert, attribute
//
// 4. after loop oldChildrenHandled = 4, oldChildrenLength = 6 (fooBAR is 6 characters)
// 4.1 fill up with two equal actions
//
// The result actions are: equal, remove, equal, insert, insert, attribute, equal, equal.
function _generateActionsFromChanges( oldChildrenLength, changes ) {
const actions = [];
let offset = 0;
let oldChildrenHandled = 0;
// Go through all buffered changes.
for ( const change of changes ) {
// First, fill "holes" between changes with "equal" actions.
if ( change.offset > offset ) {
for ( let i = 0; i < change.offset - offset; i++ ) {
actions.push( 'e' );
}
oldChildrenHandled += change.offset - offset;
}
// Then, fill up actions accordingly to change type.
if ( change.type == 'insert' ) {
for ( let i = 0; i < change.howMany; i++ ) {
actions.push( 'i' );
}
// The last handled offset is after inserted range.
offset = change.offset + change.howMany;
} else if ( change.type == 'remove' ) {
for ( let i = 0; i < change.howMany; i++ ) {
actions.push( 'r' );
}
// The last handled offset is at the position where the nodes were removed.
offset = change.offset;
// We removed `howMany` old nodes, update `oldChildrenHandled`.
oldChildrenHandled += change.howMany;
} else {
actions.push( ...'a'.repeat( change.howMany ).split( '' ) );
// The last handled offset is at the position after the changed range.
offset = change.offset + change.howMany;
// We changed `howMany` old nodes, update `oldChildrenHandled`.
oldChildrenHandled += change.howMany;
}
}
// Fill "equal" actions at the end of actions set. Use `oldChildrenHandled` to see how many children
// has not been changed / removed at the end of their parent.
if ( oldChildrenHandled < oldChildrenLength ) {
for ( let i = 0; i < oldChildrenLength - oldChildrenHandled - offset; i++ ) {
actions.push( 'e' );
}
}
return actions;
}
// Filter callback for Array.filter that filters out change entries that are in graveyard.
function _changesInGraveyardFilter( entry ) {
const posInGy = entry.position && entry.position.root.rootName == '$graveyard';
const rangeInGy = entry.range && entry.range.root.rootName == '$graveyard';
return !posInGy && !rangeInGy;
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/document.js":
/*!***********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/document.js ***!
\***********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Document; });
/* harmony import */ var _differ__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./differ */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/differ.js");
/* harmony import */ var _rootelement__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./rootelement */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/rootelement.js");
/* harmony import */ var _history__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./history */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/history.js");
/* harmony import */ var _documentselection__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./documentselection */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/documentselection.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_collection__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/collection */ "./node_modules/@ckeditor/ckeditor5-utils/src/collection.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/emittermixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_unicode__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/unicode */ "./node_modules/@ckeditor/ckeditor5-utils/src/unicode.js");
/* harmony import */ var lodash_es__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! lodash-es */ "./node_modules/lodash-es/lodash.js");
/**
* @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 engine/model/document
*/
// @if CK_DEBUG_ENGINE // const { logDocument } = require( '../dev-utils/utils' );
const graveyardName = '$graveyard';
/**
* Data model's document. It contains the model's structure, its selection and the history of changes.
*
* Read more about working with the model in
* {@glink framework/guides/architecture/editing-engine#model introduction to the the editing engine's architecture}.
*
* Usually, the document contains just one {@link module:engine/model/document~Document#roots root element}, so
* you can retrieve it by just calling {@link module:engine/model/document~Document#getRoot} without specifying its name:
*
* model.document.getRoot(); // -> returns the main root
*
* However, the document may contain multiple roots – e.g. when the editor has multiple editable areas
* (e.g. a title and a body of a message).
*
* @mixes module:utils/emittermixin~EmitterMixin
*/
class Document {
/**
* Creates an empty document instance with no {@link #roots} (other than
* the {@link #graveyard graveyard root}).
*/
constructor( model ) {
/**
* The {@link module:engine/model/model~Model model} that the document is a part of.
*
* @readonly
* @type {module:engine/model/model~Model}
*/
this.model = model;
/**
* The document version. It starts from `0` and every operation increases the version number. It is used to ensure that
* operations are applied on a proper document version.
*
* If the {@link module:engine/model/operation/operation~Operation#baseVersion base version} does not match the document version,
* a {@link module:utils/ckeditorerror~CKEditorError model-document-applyoperation-wrong-version} error is thrown.
*
* @type {Number}
*/
this.version = 0;
/**
* The document's history.
*
* @readonly
* @type {module:engine/model/history~History}
*/
this.history = new _history__WEBPACK_IMPORTED_MODULE_2__["default"]( this );
/**
* The selection in this document.
*
* @readonly
* @type {module:engine/model/documentselection~DocumentSelection}
*/
this.selection = new _documentselection__WEBPACK_IMPORTED_MODULE_3__["default"]( this );
/**
* A list of roots that are owned and managed by this document. Use {@link #createRoot} and
* {@link #getRoot} to manipulate it.
*
* @readonly
* @type {module:utils/collection~Collection}
*/
this.roots = new _ckeditor_ckeditor5_utils_src_collection__WEBPACK_IMPORTED_MODULE_4__["default"]( { idProperty: 'rootName' } );
/**
* The model differ object. Its role is to buffer changes done on the model document and then calculate a diff of those changes.
*
* @readonly
* @type {module:engine/model/differ~Differ}
*/
this.differ = new _differ__WEBPACK_IMPORTED_MODULE_0__["default"]( model.markers );
/**
* Post-fixer callbacks registered to the model document.
*
* @private
* @type {Set.}
*/
this._postFixers = new Set();
/**
* A boolean indicates whether the selection has changed until
*
* @private
* @type {Boolean}
*/
this._hasSelectionChangedFromTheLastChangeBlock = false;
// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
this.createRoot( '$root', graveyardName );
// First, if the operation is a document operation check if it's base version is correct.
this.listenTo( model, 'applyOperation', ( evt, args ) => {
const operation = args[ 0 ];
if ( operation.isDocumentOperation && operation.baseVersion !== this.version ) {
/**
* Only operations with matching versions can be applied.
*
* @error model-document-applyoperation-wrong-version
* @param {module:engine/model/operation/operation~Operation} operation
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_6__["default"]( 'model-document-applyoperation-wrong-version', this, { operation } );
}
}, { priority: 'highest' } );
// Then, still before an operation is applied on model, buffer the change in differ.
this.listenTo( model, 'applyOperation', ( evt, args ) => {
const operation = args[ 0 ];
if ( operation.isDocumentOperation ) {
this.differ.bufferOperation( operation );
}
}, { priority: 'high' } );
// After the operation is applied, bump document's version and add the operation to the history.
this.listenTo( model, 'applyOperation', ( evt, args ) => {
const operation = args[ 0 ];
if ( operation.isDocumentOperation ) {
this.version++;
this.history.addOperation( operation );
}
}, { priority: 'low' } );
// Listen to selection changes. If selection changed, mark it.
this.listenTo( this.selection, 'change', () => {
this._hasSelectionChangedFromTheLastChangeBlock = true;
} );
// Buffer marker changes.
// This is not covered in buffering operations because markers may change outside of them (when they
// are modified using `model.markers` collection, not through `MarkerOperation`).
this.listenTo( model.markers, 'update', ( evt, marker, oldRange, newRange ) => {
// Whenever marker is updated, buffer that change.
this.differ.bufferMarkerChange( marker.name, oldRange, newRange, marker.affectsData );
if ( oldRange === null ) {
// If this is a new marker, add a listener that will buffer change whenever marker changes.
marker.on( 'change', ( evt, oldRange ) => {
this.differ.bufferMarkerChange( marker.name, oldRange, marker.getRange(), marker.affectsData );
} );
}
} );
}
/**
* The graveyard tree root. A document always has a graveyard root that stores removed nodes.
*
* @readonly
* @member {module:engine/model/rootelement~RootElement}
*/
get graveyard() {
return this.getRoot( graveyardName );
}
/**
* Creates a new root.
*
* @param {String} [elementName='$root'] The element name. Defaults to `'$root'` which also has some basic schema defined
* (`$block`s are allowed inside the `$root`). Make sure to define a proper schema if you use a different name.
* @param {String} [rootName='main'] A unique root name.
* @returns {module:engine/model/rootelement~RootElement} The created root.
*/
createRoot( elementName = '$root', rootName = 'main' ) {
if ( this.roots.get( rootName ) ) {
/**
* A root with the specified name already exists.
*
* @error model-document-createroot-name-exists
* @param {module:engine/model/document~Document} doc
* @param {String} name
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_6__["default"]( 'model-document-createroot-name-exists', this, { name: rootName } );
}
const root = new _rootelement__WEBPACK_IMPORTED_MODULE_1__["default"]( this, elementName, rootName );
this.roots.add( root );
return root;
}
/**
* Removes all event listeners set by the document instance.
*/
destroy() {
this.selection.destroy();
this.stopListening();
}
/**
* Returns a root by its name.
*
* @param {String} [name='main'] A unique root name.
* @returns {module:engine/model/rootelement~RootElement|null} The root registered under a given name or `null` when
* there is no root with the given name.
*/
getRoot( name = 'main' ) {
return this.roots.get( name );
}
/**
* Returns an array with names of all roots (without the {@link #graveyard}) added to the document.
*
* @returns {Array.} Roots names.
*/
getRootNames() {
return Array.from( this.roots, root => root.rootName ).filter( name => name != graveyardName );
}
/**
* Used to register a post-fixer callback. A post-fixer mechanism guarantees that the features
* will operate on a correct model state.
*
* An execution of a feature may lead to an incorrect document tree state. The callbacks are used to fix the document tree after
* it has changed. Post-fixers are fired just after all changes from the outermost change block were applied but
* before the {@link module:engine/model/document~Document#event:change change event} is fired. If a post-fixer callback made
* a change, it should return `true`. When this happens, all post-fixers are fired again to check if something else should
* not be fixed in the new document tree state.
*
* As a parameter, a post-fixer callback receives a {@link module:engine/model/writer~Writer writer} instance connected with the
* executed changes block. Thanks to that, all changes done by the callback will be added to the same
* {@link module:engine/model/batch~Batch batch} (and undo step) as the original changes. This makes post-fixer changes transparent
* for the user.
*
* An example of a post-fixer is a callback that checks if all the data were removed from the editor. If so, the
* callback should add an empty paragraph so that the editor is never empty:
*
* document.registerPostFixer( writer => {
* const changes = document.differ.getChanges();
*
* // Check if the changes lead to an empty root in the editor.
* for ( const entry of changes ) {
* if ( entry.type == 'remove' && entry.position.root.isEmpty ) {
* writer.insertElement( 'paragraph', entry.position.root, 0 );
*
* // It is fine to return early, even if multiple roots would need to be fixed.
* // All post-fixers will be fired again, so if there are more empty roots, those will be fixed, too.
* return true;
* }
* }
* } );
*
* @param {Function} postFixer
*/
registerPostFixer( postFixer ) {
this._postFixers.add( postFixer );
}
/**
* A custom `toJSON()` method to solve child-parent circular dependencies.
*
* @returns {Object} A clone of this object with the document property changed to a string.
*/
toJSON() {
const json = Object(lodash_es__WEBPACK_IMPORTED_MODULE_9__["clone"])( this );
// Due to circular references we need to remove parent reference.
json.selection = '[engine.model.DocumentSelection]';
json.model = '[engine.model.Model]';
return json;
}
/**
* Check if there were any changes done on document, and if so, call post-fixers,
* fire `change` event for features and conversion and then reset the differ.
* Fire `change:data` event when at least one operation or buffered marker changes the data.
*
* @protected
* @fires change
* @fires change:data
* @param {module:engine/model/writer~Writer} writer The writer on which post-fixers will be called.
*/
_handleChangeBlock( writer ) {
if ( this._hasDocumentChangedFromTheLastChangeBlock() ) {
this._callPostFixers( writer );
// Refresh selection attributes according to the final position in the model after the change.
this.selection.refresh();
if ( this.differ.hasDataChanges() ) {
this.fire( 'change:data', writer.batch );
} else {
this.fire( 'change', writer.batch );
}
// Theoretically, it is not necessary to refresh selection after change event because
// post-fixers are the last who should change the model, but just in case...
this.selection.refresh();
this.differ.reset();
}
this._hasSelectionChangedFromTheLastChangeBlock = false;
}
/**
* Returns whether there is a buffered change or if the selection has changed from the last
* {@link module:engine/model/model~Model#enqueueChange `enqueueChange()` block}
* or {@link module:engine/model/model~Model#change `change()` block}.
*
* @protected
* @returns {Boolean} Returns `true` if document has changed from the last `change()` or `enqueueChange()` block.
*/
_hasDocumentChangedFromTheLastChangeBlock() {
return !this.differ.isEmpty || this._hasSelectionChangedFromTheLastChangeBlock;
}
/**
* Returns the default root for this document which is either the first root that was added to the document using
* {@link #createRoot} or the {@link #graveyard graveyard root} if no other roots were created.
*
* @protected
* @returns {module:engine/model/rootelement~RootElement} The default root for this document.
*/
_getDefaultRoot() {
for ( const root of this.roots ) {
if ( root !== this.graveyard ) {
return root;
}
}
return this.graveyard;
}
/**
* Returns the default range for this selection. The default range is a collapsed range that starts and ends
* at the beginning of this selection's document {@link #_getDefaultRoot default root}.
*
* @protected
* @returns {module:engine/model/range~Range}
*/
_getDefaultRange() {
const defaultRoot = this._getDefaultRoot();
const model = this.model;
const schema = model.schema;
// Find the first position where the selection can be put.
const position = model.createPositionFromPath( defaultRoot, [ 0 ] );
const nearestRange = schema.getNearestSelectionRange( position );
// If valid selection range is not found - return range collapsed at the beginning of the root.
return nearestRange || model.createRange( position );
}
/**
* Checks whether a given {@link module:engine/model/range~Range range} is a valid range for
* the {@link #selection document's selection}.
*
* @private
* @param {module:engine/model/range~Range} range A range to check.
* @returns {Boolean} `true` if `range` is valid, `false` otherwise.
*/
_validateSelectionRange( range ) {
return validateTextNodePosition( range.start ) && validateTextNodePosition( range.end );
}
/**
* Performs post-fixer loops. Executes post-fixer callbacks as long as none of them has done any changes to the model.
*
* @private
* @param {module:engine/model/writer~Writer} writer The writer on which post-fixer callbacks will be called.
*/
_callPostFixers( writer ) {
let wasFixed = false;
do {
for ( const callback of this._postFixers ) {
// Ensure selection attributes are up to date before each post-fixer.
// https://github.com/ckeditor/ckeditor5-engine/issues/1673.
//
// It might be good to refresh the selection after each operation but at the moment it leads
// to losing attributes for composition or and spell checking
// https://github.com/ckeditor/ckeditor5-typing/issues/188
this.selection.refresh();
wasFixed = callback( writer );
if ( wasFixed ) {
break;
}
}
} while ( wasFixed );
}
/**
* Fired after each {@link module:engine/model/model~Model#enqueueChange `enqueueChange()` block} or the outermost
* {@link module:engine/model/model~Model#change `change()` block} was executed and the document was changed
* during that block's execution.
*
* The changes which this event will cover include:
*
* * document structure changes,
* * selection changes,
* * marker changes.
*
* If you want to be notified about all these changes, then simply listen to this event like this:
*
* model.document.on( 'change', () => {
* console.log( 'The document has changed!' );
* } );
*
* If, however, you only want to be notified about the data changes, then use the
* {@link module:engine/model/document~Document#event:change:data change:data} event,
* which is fired for document structure changes and marker changes (which affects the data).
*
* model.document.on( 'change:data', () => {
* console.log( 'The data has changed!' );
* } );
*
* @event change
* @param {module:engine/model/batch~Batch} batch The batch that was used in the executed changes block.
*/
/**
* It is a narrower version of the {@link #event:change} event. It is fired for changes which
* affect the editor data. This is:
*
* * document structure changes,
* * marker changes (which affects the data).
*
* If you want to be notified about the data changes, then listen to this event:
*
* model.document.on( 'change:data', () => {
* console.log( 'The data has changed!' );
* } );
*
* If you would like to listen to all document changes, then check out the
* {@link module:engine/model/document~Document#event:change change} event.
*
* @event change:data
* @param {module:engine/model/batch~Batch} batch The batch that was used in the executed changes block.
*/
// @if CK_DEBUG_ENGINE // log( version = null ) {
// @if CK_DEBUG_ENGINE // version = version === null ? this.version : version;
// @if CK_DEBUG_ENGINE // logDocument( this, version );
// @if CK_DEBUG_ENGINE // }
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_7__["default"])( Document, _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_5__["default"] );
// Checks whether given range boundary position is valid for document selection, meaning that is not between
// unicode surrogate pairs or base character and combining marks.
function validateTextNodePosition( rangeBoundary ) {
const textNode = rangeBoundary.textNode;
if ( textNode ) {
const data = textNode.data;
const offset = rangeBoundary.offset - textNode.startOffset;
return !Object(_ckeditor_ckeditor5_utils_src_unicode__WEBPACK_IMPORTED_MODULE_8__["isInsideSurrogatePair"])( data, offset ) && !Object(_ckeditor_ckeditor5_utils_src_unicode__WEBPACK_IMPORTED_MODULE_8__["isInsideCombinedSymbol"])( data, offset );
}
return true;
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/documentfragment.js":
/*!*******************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/documentfragment.js ***!
\*******************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return DocumentFragment; });
/* harmony import */ var _nodelist__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./nodelist */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js");
/* harmony import */ var _element__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./element */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js");
/* harmony import */ var _text__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./text */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js");
/* harmony import */ var _textproxy__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./textproxy */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/textproxy.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_isiterable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/isiterable */ "./node_modules/@ckeditor/ckeditor5-utils/src/isiterable.js");
/**
* @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 module:engine/model/documentfragment
*/
// @if CK_DEBUG_ENGINE // const { stringifyMap } = require( '../dev-utils/utils' );
/**
* DocumentFragment represents a part of model which does not have a common root but it's top-level nodes
* can be seen as siblings. In other words, it is a detached part of model tree, without a root.
*
* DocumentFragment has own {@link module:engine/model/markercollection~MarkerCollection}. Markers from this collection
* will be set to the {@link module:engine/model/model~Model#markers model markers} by a
* {@link module:engine/model/writer~Writer#insert} function.
*/
class DocumentFragment {
/**
* Creates an empty `DocumentFragment`.
*
* **Note:** Constructor of this class shouldn't be used directly in the code.
* Use the {@link module:engine/model/writer~Writer#createDocumentFragment} method instead.
*
* @protected
* @param {module:engine/model/node~Node|Iterable.} [children]
* Nodes to be contained inside the `DocumentFragment`.
*/
constructor( children ) {
/**
* DocumentFragment static markers map. This is a list of names and {@link module:engine/model/range~Range ranges}
* which will be set as Markers to {@link module:engine/model/model~Model#markers model markers collection}
* when DocumentFragment will be inserted to the document.
*
* @readonly
* @member {Map} module:engine/model/documentfragment~DocumentFragment#markers
*/
this.markers = new Map();
/**
* List of nodes contained inside the document fragment.
*
* @private
* @member {module:engine/model/nodelist~NodeList} module:engine/model/documentfragment~DocumentFragment#_children
*/
this._children = new _nodelist__WEBPACK_IMPORTED_MODULE_0__["default"]();
if ( children ) {
this._insertChild( 0, children );
}
}
/**
* Returns an iterator that iterates over all nodes contained inside this document fragment.
*
* @returns {Iterable.}
*/
[ Symbol.iterator ]() {
return this.getChildren();
}
/**
* Number of this document fragment's children.
*
* @readonly
* @type {Number}
*/
get childCount() {
return this._children.length;
}
/**
* Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all of this document fragment's children.
*
* @readonly
* @type {Number}
*/
get maxOffset() {
return this._children.maxOffset;
}
/**
* Is `true` if there are no nodes inside this document fragment, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isEmpty() {
return this.childCount === 0;
}
/**
* Artificial root of `DocumentFragment`. Returns itself. Added for compatibility reasons.
*
* @readonly
* @type {module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
return this;
}
/**
* Artificial parent of `DocumentFragment`. Returns `null`. Added for compatibility reasons.
*
* @readonly
* @type {null}
*/
get parent() {
return null;
}
/**
* Checks whether this object is of the given type.
*
* docFrag.is( 'documentFragment' ); // -> true
* docFrag.is( 'model:documentFragment' ); // -> true
*
* docFrag.is( 'view:documentFragment' ); // -> false
* docFrag.is( 'element' ); // -> false
* docFrag.is( 'node' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'documentFragment' || type === 'model:documentFragment';
}
/**
* Gets the child at the given index. Returns `null` if incorrect index was passed.
*
* @param {Number} index Index of child.
* @returns {module:engine/model/node~Node|null} Child node.
*/
getChild( index ) {
return this._children.getNode( index );
}
/**
* Returns an iterator that iterates over all of this document fragment's children.
*
* @returns {Iterable.}
*/
getChildren() {
return this._children[ Symbol.iterator ]();
}
/**
* Returns an index of the given child node. Returns `null` if given node is not a child of this document fragment.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number|null} Child node's index.
*/
getChildIndex( node ) {
return this._children.getNodeIndex( node );
}
/**
* Returns the starting offset of given child. Starting offset is equal to the sum of
* {@link module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
* given node is not a child of this document fragment.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number|null} Child node's starting offset.
*/
getChildStartOffset( node ) {
return this._children.getNodeStartOffset( node );
}
/**
* Returns path to a `DocumentFragment`, which is an empty array. Added for compatibility reasons.
*
* @returns {Array}
*/
getPath() {
return [];
}
/**
* Returns a descendant node by its path relative to this element.
*
* // ac
* this.getNodeByPath( [ 0 ] ); // -> "a"
* this.getNodeByPath( [ 1 ] ); // ->
* this.getNodeByPath( [ 1, 0 ] ); // -> "c"
*
* @param {Array.} relativePath Path of the node to find, relative to this element.
* @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
*/
getNodeByPath( relativePath ) {
let node = this; // eslint-disable-line consistent-this
for ( const index of relativePath ) {
node = node.getChild( node.offsetToIndex( index ) );
}
return node;
}
/**
* Converts offset "position" to index "position".
*
* Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
* too high, returns index after last child}.
*
* const textNode = new Text( 'foo' );
* const pElement = new Element( 'p' );
* const docFrag = new DocumentFragment( [ textNode, pElement ] );
* docFrag.offsetToIndex( -1 ); // Returns 0, because offset is too low.
* docFrag.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
* docFrag.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
* docFrag.offsetToIndex( 2 ); // Returns 0.
* docFrag.offsetToIndex( 3 ); // Returns 1.
* docFrag.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
*
* @param {Number} offset Offset to look for.
* @returns {Number} Index of a node that occupies given offset.
*/
offsetToIndex( offset ) {
return this._children.offsetToIndex( offset );
}
/**
* Converts `DocumentFragment` instance to plain object and returns it.
* Takes care of converting all of this document fragment's children.
*
* @returns {Object} `DocumentFragment` instance converted to plain object.
*/
toJSON() {
const json = [];
for ( const node of this._children ) {
json.push( node.toJSON() );
}
return json;
}
/**
* Creates a `DocumentFragment` instance from given plain object (i.e. parsed JSON string).
* Converts `DocumentFragment` children to proper nodes.
*
* @param {Object} json Plain object to be converted to `DocumentFragment`.
* @returns {module:engine/model/documentfragment~DocumentFragment} `DocumentFragment` instance created using given plain object.
*/
static fromJSON( json ) {
const children = [];
for ( const child of json ) {
if ( child.name ) {
// If child has name property, it is an Element.
children.push( _element__WEBPACK_IMPORTED_MODULE_1__["default"].fromJSON( child ) );
} else {
// Otherwise, it is a Text node.
children.push( _text__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( child ) );
}
}
return new DocumentFragment( children );
}
/**
* {@link #_insertChild Inserts} one or more nodes at the end of this document fragment.
*
* @protected
* @param {module:engine/model/item~Item|Iterable.} items Items to be inserted.
*/
_appendChild( items ) {
this._insertChild( this.childCount, items );
}
/**
* Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
* to this document fragment.
*
* @protected
* @param {Number} index Index at which nodes should be inserted.
* @param {module:engine/model/item~Item|Iterable.} items Items to be inserted.
*/
_insertChild( index, items ) {
const nodes = normalize( items );
for ( const node of nodes ) {
// If node that is being added to this element is already inside another element, first remove it from the old parent.
if ( node.parent !== null ) {
node._remove();
}
node.parent = this;
}
this._children._insertNodes( index, nodes );
}
/**
* Removes one or more nodes starting at the given index
* and sets {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
*
* @protected
* @param {Number} index Index of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.} Array containing removed nodes.
*/
_removeChildren( index, howMany = 1 ) {
const nodes = this._children._removeNodes( index, howMany );
for ( const node of nodes ) {
node.parent = null;
}
return nodes;
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return 'documentFragment';
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // log() {
// @if CK_DEBUG_ENGINE // console.log( 'ModelDocumentFragment: ' + this );
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // printTree() {
// @if CK_DEBUG_ENGINE // let string = 'ModelDocumentFragment: [';
// @if CK_DEBUG_ENGINE // for ( const child of this.getChildren() ) {
// @if CK_DEBUG_ENGINE // string += '\n';
// @if CK_DEBUG_ENGINE // if ( child.is( '$text' ) ) {
// @if CK_DEBUG_ENGINE // const textAttrs = stringifyMap( child._attrs );
// @if CK_DEBUG_ENGINE // string += '\t'.repeat( 1 );
// @if CK_DEBUG_ENGINE // if ( textAttrs !== '' ) {
// @if CK_DEBUG_ENGINE // string += `<$text${ textAttrs }>` + child.data + '$text>';
// @if CK_DEBUG_ENGINE // } else {
// @if CK_DEBUG_ENGINE // string += child.data;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // } else {
// @if CK_DEBUG_ENGINE // string += child.printTree( 1 );
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // string += '\n]';
// @if CK_DEBUG_ENGINE // return string;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // logTree() {
// @if CK_DEBUG_ENGINE // console.log( this.printTree() );
// @if CK_DEBUG_ENGINE // }
}
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/model/item~Item|Iterable.}
// @returns {Iterable.}
function normalize( nodes ) {
// Separate condition because string is iterable.
if ( typeof nodes == 'string' ) {
return [ new _text__WEBPACK_IMPORTED_MODULE_2__["default"]( nodes ) ];
}
if ( !Object(_ckeditor_ckeditor5_utils_src_isiterable__WEBPACK_IMPORTED_MODULE_4__["default"])( nodes ) ) {
nodes = [ nodes ];
}
// Array.from to enable .map() on non-arrays.
return Array.from( nodes )
.map( node => {
if ( typeof node == 'string' ) {
return new _text__WEBPACK_IMPORTED_MODULE_2__["default"]( node );
}
if ( node instanceof _textproxy__WEBPACK_IMPORTED_MODULE_3__["default"] ) {
return new _text__WEBPACK_IMPORTED_MODULE_2__["default"]( node.data, node.getAttributes() );
}
return node;
} );
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/documentselection.js":
/*!********************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/documentselection.js ***!
\********************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return DocumentSelection; });
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/emittermixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js");
/* harmony import */ var _selection__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./selection */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/selection.js");
/* harmony import */ var _liverange__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./liverange */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/liverange.js");
/* harmony import */ var _text__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./text */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js");
/* harmony import */ var _textproxy__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./textproxy */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/textproxy.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_tomap__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/tomap */ "./node_modules/@ckeditor/ckeditor5-utils/src/tomap.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_collection__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/collection */ "./node_modules/@ckeditor/ckeditor5-utils/src/collection.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_uid__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/uid */ "./node_modules/@ckeditor/ckeditor5-utils/src/uid.js");
/**
* @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 engine/model/documentselection
*/
const storePrefix = 'selection:';
/**
* `DocumentSelection` is a special selection which is used as the
* {@link module:engine/model/document~Document#selection document's selection}.
* There can be only one instance of `DocumentSelection` per document.
*
* Document selection can only be changed by using the {@link module:engine/model/writer~Writer} instance
* inside the {@link module:engine/model/model~Model#change `change()`} block, as it provides a secure way to modify model.
*
* `DocumentSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
* to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
*
* Differences between {@link module:engine/model/selection~Selection} and `DocumentSelection` are:
* * there is always a range in `DocumentSelection` - even if no ranges were added there is a "default range"
* present in the selection,
* * ranges added to this selection updates automatically when the document changes,
* * attributes of `DocumentSelection` are updated automatically according to selection ranges.
*
* Since `DocumentSelection` uses {@link module:engine/model/liverange~LiveRange live ranges}
* and is updated when {@link module:engine/model/document~Document document}
* changes, it cannot be set on {@link module:engine/model/node~Node nodes}
* that are inside {@link module:engine/model/documentfragment~DocumentFragment document fragment}.
* If you need to represent a selection in document fragment,
* use {@link module:engine/model/selection~Selection Selection class} instead.
*
* @mixes module:utils/emittermixin~EmitterMixin
*/
class DocumentSelection {
/**
* Creates an empty live selection for given {@link module:engine/model/document~Document}.
*
* @param {module:engine/model/document~Document} doc Document which owns this selection.
*/
constructor( doc ) {
/**
* Selection used internally by that class (`DocumentSelection` is a proxy to that selection).
*
* @protected
*/
this._selection = new LiveSelection( doc );
this._selection.delegate( 'change:range' ).to( this );
this._selection.delegate( 'change:attribute' ).to( this );
this._selection.delegate( 'change:marker' ).to( this );
}
/**
* Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
* collapsed.
*
* @readonly
* @type {Boolean}
*/
get isCollapsed() {
return this._selection.isCollapsed;
}
/**
* Selection anchor. Anchor may be described as a position where the most recent part of the selection starts.
* Together with {@link #focus} they define the direction of selection, which is important
* when expanding/shrinking selection. Anchor is always {@link module:engine/model/range~Range#start start} or
* {@link module:engine/model/range~Range#end end} position of the most recently added range.
*
* Is set to `null` if there are no ranges in selection.
*
* @see #focus
* @readonly
* @type {module:engine/model/position~Position|null}
*/
get anchor() {
return this._selection.anchor;
}
/**
* Selection focus. Focus is a position where the selection ends.
*
* Is set to `null` if there are no ranges in selection.
*
* @see #anchor
* @readonly
* @type {module:engine/model/position~Position|null}
*/
get focus() {
return this._selection.focus;
}
/**
* Returns number of ranges in selection.
*
* @readonly
* @type {Number}
*/
get rangeCount() {
return this._selection.rangeCount;
}
/**
* Describes whether `Documentselection` has own range(s) set, or if it is defaulted to
* {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
*
* @readonly
* @type {Boolean}
*/
get hasOwnRange() {
return this._selection.hasOwnRange;
}
/**
* Specifies whether the {@link #focus}
* precedes {@link #anchor}.
*
* @readonly
* @type {Boolean}
*/
get isBackward() {
return this._selection.isBackward;
}
/**
* Describes whether the gravity is overridden (using {@link module:engine/model/writer~Writer#overrideSelectionGravity}) or not.
*
* Note that the gravity remains overridden as long as will not be restored the same number of times as it was overridden.
*
* @readonly
* @returns {Boolean}
*/
get isGravityOverridden() {
return this._selection.isGravityOverridden;
}
/**
* A collection of selection markers.
* Marker is a selection marker when selection range is inside the marker range.
*
* @readonly
* @type {module:utils/collection~Collection.}
*/
get markers() {
return this._selection.markers;
}
/**
* Used for the compatibility with the {@link module:engine/model/selection~Selection#isEqual} method.
*
* @protected
*/
get _ranges() {
return this._selection._ranges;
}
/**
* Returns an iterable that iterates over copies of selection ranges.
*
* @returns {Iterable.}
*/
getRanges() {
return this._selection.getRanges();
}
/**
* Returns the first position in the selection.
* First position is the position that {@link module:engine/model/position~Position#isBefore is before}
* any other position in the selection.
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/position~Position|null}
*/
getFirstPosition() {
return this._selection.getFirstPosition();
}
/**
* Returns the last position in the selection.
* Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
* any other position in the selection.
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/position~Position|null}
*/
getLastPosition() {
return this._selection.getLastPosition();
}
/**
* Returns a copy of the first range in the selection.
* First range is the one which {@link module:engine/model/range~Range#start start} position
* {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
* (not to confuse with the first range added to the selection).
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/range~Range|null}
*/
getFirstRange() {
return this._selection.getFirstRange();
}
/**
* Returns a copy of the last range in the selection.
* Last range is the one which {@link module:engine/model/range~Range#end end} position
* {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
* recently added to the selection).
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/range~Range|null}
*/
getLastRange() {
return this._selection.getLastRange();
}
/**
* Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
*
* This method's result can be used for example to apply block styling to all blocks covered by this selection.
*
* **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
* but will not return blocks nested in other blocks.
*
* In this case the function will return exactly all 3 paragraphs (note: `` is not a block itself):
*
* [a
*
* b
*
* c]d
*
* In this case the paragraph will also be returned, despite the collapsed selection:
*
* []a
*
* In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B:
*
* [
*
*
*
*
* ]
*
* If the selection is inside a block all the inner blocks (A & B) are returned:
*
*
* [a
* b]
*
*
* **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
* this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
*
* [a
* b
* ]c // this block will not be returned
*
* @returns {Iterable.}
*/
getSelectedBlocks() {
return this._selection.getSelectedBlocks();
}
/**
* Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
* one range in the selection, and that range contains exactly one element.
* Returns `null` if there is no selected element.
*
* @returns {module:engine/model/element~Element|null}
*/
getSelectedElement() {
return this._selection.getSelectedElement();
}
/**
* Checks whether the selection contains the entire content of the given element. This means that selection must start
* at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
* touching the element's end.
*
* By default, this method will check whether the entire content of the selection's current root is selected.
* Useful to check if e.g. the user has just pressed Ctrl + A .
*
* @param {module:engine/model/element~Element} [element=this.anchor.root]
* @returns {Boolean}
*/
containsEntireContent( element ) {
return this._selection.containsEntireContent( element );
}
/**
* Unbinds all events previously bound by document selection.
*/
destroy() {
this._selection.destroy();
}
/**
* Returns iterable that iterates over this selection's attribute keys.
*
* @returns {Iterable.}
*/
getAttributeKeys() {
return this._selection.getAttributeKeys();
}
/**
* Returns iterable that iterates over this selection's attributes.
*
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
*
* @returns {Iterable.<*>}
*/
getAttributes() {
return this._selection.getAttributes();
}
/**
* Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
*
* @param {String} key Key of attribute to look for.
* @returns {*} Attribute value or `undefined`.
*/
getAttribute( key ) {
return this._selection.getAttribute( key );
}
/**
* Checks if the selection has an attribute for given key.
*
* @param {String} key Key of attribute to check.
* @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
*/
hasAttribute( key ) {
return this._selection.hasAttribute( key );
}
/**
* Refreshes selection attributes and markers according to the current position in the model.
*/
refresh() {
this._selection._updateMarkers();
this._selection._updateAttributes( false );
}
/**
* Checks whether this object is of the given type.
*
* selection.is( 'selection' ); // -> true
* selection.is( 'documentSelection' ); // -> true
* selection.is( 'model:selection' ); // -> true
* selection.is( 'model:documentSelection' ); // -> true
*
* selection.is( 'view:selection' ); // -> false
* selection.is( 'element' ); // -> false
* selection.is( 'node' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'selection' ||
type == 'model:selection' ||
type == 'documentSelection' ||
type == 'model:documentSelection';
}
/**
* Moves {@link module:engine/model/documentselection~DocumentSelection#focus} to the specified location.
* Should be used only within the {@link module:engine/model/writer~Writer#setSelectionFocus} method.
*
* The location can be specified in the same form as
* {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
*
* @see module:engine/model/writer~Writer#setSelectionFocus
* @protected
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/model/item~Item model item}.
*/
_setFocus( itemOrPosition, offset ) {
this._selection.setFocus( itemOrPosition, offset );
}
/**
* Sets this selection's ranges and direction to the specified location based on the given
* {@link module:engine/model/selection~Selectable selectable}.
* Should be used only within the {@link module:engine/model/writer~Writer#setSelection} method.
*
* @see module:engine/model/writer~Writer#setSelection
* @protected
* @param {module:engine/model/selection~Selectable} selectable
* @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
* @param {Object} [options]
* @param {Boolean} [options.backward] Sets this selection instance to be backward.
*/
_setTo( selectable, placeOrOffset, options ) {
this._selection.setTo( selectable, placeOrOffset, options );
}
/**
* Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
* Should be used only within the {@link module:engine/model/writer~Writer#setSelectionAttribute} method.
*
* @see module:engine/model/writer~Writer#setSelectionAttribute
* @protected
* @param {String} key Key of the attribute to set.
* @param {*} value Attribute value.
*/
_setAttribute( key, value ) {
this._selection.setAttribute( key, value );
}
/**
* Removes an attribute with given key from the selection.
* If the given attribute was set on the selection, fires the {@link module:engine/model/selection~Selection#event:change:range}
* event with removed attribute key.
* Should be used only within the {@link module:engine/model/writer~Writer#removeSelectionAttribute} method.
*
* @see module:engine/model/writer~Writer#removeSelectionAttribute
* @protected
* @param {String} key Key of the attribute to remove.
*/
_removeAttribute( key ) {
this._selection.removeAttribute( key );
}
/**
* Returns an iterable that iterates through all selection attributes stored in current selection's parent.
*
* @protected
* @returns {Iterable.<*>}
*/
_getStoredAttributes() {
return this._selection._getStoredAttributes();
}
/**
* Temporarily changes the gravity of the selection from the left to the right.
*
* The gravity defines from which direction the selection inherits its attributes. If it's the default left
* gravity, the selection (after being moved by the the user) inherits attributes from its left hand side.
* This method allows to temporarily override this behavior by forcing the gravity to the right.
*
* It returns an unique identifier which is required to restore the gravity. It guarantees the symmetry
* of the process.
*
* @see module:engine/model/writer~Writer#overrideSelectionGravity
* @protected
* @returns {String} The unique id which allows restoring the gravity.
*/
_overrideGravity() {
return this._selection.overrideGravity();
}
/**
* Restores the {@link ~DocumentSelection#_overrideGravity overridden gravity}.
*
* Restoring the gravity is only possible using the unique identifier returned by
* {@link ~DocumentSelection#_overrideGravity}. Note that the gravity remains overridden as long as won't be restored
* the same number of times it was overridden.
*
* @see module:engine/model/writer~Writer#restoreSelectionGravity
* @protected
* @param {String} uid The unique id returned by {@link #_overrideGravity}.
*/
_restoreGravity( uid ) {
this._selection.restoreGravity( uid );
}
/**
* Generates and returns an attribute key for selection attributes store, basing on original attribute key.
*
* @protected
* @param {String} key Attribute key to convert.
* @returns {String} Converted attribute key, applicable for selection store.
*/
static _getStoreAttributeKey( key ) {
return storePrefix + key;
}
/**
* Checks whether the given attribute key is an attribute stored on an element.
*
* @protected
* @param {String} key
* @returns {Boolean}
*/
static _isStoreAttributeKey( key ) {
return key.startsWith( storePrefix );
}
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_0__["default"])( DocumentSelection, _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__["default"] );
/**
* Fired when selection range(s) changed.
*
* @event change:range
* @param {Boolean} directChange In case of {@link module:engine/model/selection~Selection} class it is always set
* to `true` which indicates that the selection change was caused by a direct use of selection's API.
* The {@link module:engine/model/documentselection~DocumentSelection}, however, may change because its position
* was directly changed through the {@link module:engine/model/writer~Writer writer} or because its position was
* changed because the structure of the model has been changed (which means an indirect change).
* The indirect change does not occur in case of normal (detached) selections because they are "static" (as "not live")
* which mean that they are not updated once the document changes.
*/
/**
* Fired when selection attribute changed.
*
* @event change:attribute
* @param {Boolean} directChange In case of {@link module:engine/model/selection~Selection} class it is always set
* to `true` which indicates that the selection change was caused by a direct use of selection's API.
* The {@link module:engine/model/documentselection~DocumentSelection}, however, may change because its attributes
* were directly changed through the {@link module:engine/model/writer~Writer writer} or because its position was
* changed in the model and its attributes were refreshed (which means an indirect change).
* The indirect change does not occur in case of normal (detached) selections because they are "static" (as "not live")
* which mean that they are not updated once the document changes.
* @param {Array.} attributeKeys Array containing keys of attributes that changed.
*/
/**
* Fired when selection marker(s) changed.
*
* @event change:marker
* @param {Boolean} directChange This is always set to `false` in case of `change:marker` event as there is no possibility
* to change markers directly through {@link module:engine/model/documentselection~DocumentSelection} API.
* See also {@link module:engine/model/documentselection~DocumentSelection#event:change:range} and
* {@link module:engine/model/documentselection~DocumentSelection#event:change:attribute}.
* @param {Array.} oldMarkers Markers in which the selection was before the change.
*/
// `LiveSelection` is used internally by {@link module:engine/model/documentselection~DocumentSelection} and shouldn't be used directly.
//
// LiveSelection` is automatically updated upon changes in the {@link module:engine/model/document~Document document}
// to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
//
// Differences between {@link module:engine/model/selection~Selection} and `LiveSelection` are:
// * there is always a range in `LiveSelection` - even if no ranges were added there is a "default range"
// present in the selection,
// * ranges added to this selection updates automatically when the document changes,
// * attributes of `LiveSelection` are updated automatically according to selection ranges.
//
// @extends module:engine/model/selection~Selection
//
class LiveSelection extends _selection__WEBPACK_IMPORTED_MODULE_2__["default"] {
// Creates an empty live selection for given {@link module:engine/model/document~Document}.
// @param {module:engine/model/document~Document} doc Document which owns this selection.
constructor( doc ) {
super();
// List of selection markers.
// Marker is a selection marker when selection range is inside the marker range.
//
// @type {module:utils/collection~Collection}
this.markers = new _ckeditor_ckeditor5_utils_src_collection__WEBPACK_IMPORTED_MODULE_7__["default"]( { idProperty: 'name' } );
// Document which owns this selection.
//
// @protected
// @member {module:engine/model/model~Model}
this._model = doc.model;
// Document which owns this selection.
//
// @protected
// @member {module:engine/model/document~Document}
this._document = doc;
// Keeps mapping of attribute name to priority with which the attribute got modified (added/changed/removed)
// last time. Possible values of priority are: `'low'` and `'normal'`.
//
// Priorities are used by internal `LiveSelection` mechanisms. All attributes set using `LiveSelection`
// attributes API are set with `'normal'` priority.
//
// @private
// @member {Map} module:engine/model/liveselection~LiveSelection#_attributePriority
this._attributePriority = new Map();
// Position to which the selection should be set if the last selection range was moved to the graveyard.
// @private
// @member {module:engine/model/position~Position} module:engine/model/liveselection~LiveSelection#_selectionRestorePosition
this._selectionRestorePosition = null;
// Flag that informs whether the selection ranges have changed. It is changed on true when `LiveRange#change:range` event is fired.
// @private
// @member {Array} module:engine/model/liveselection~LiveSelection#_hasChangedRange
this._hasChangedRange = false;
// Each overriding gravity adds an UID to the set and each removal removes it.
// Gravity is overridden when there's at least one UID in the set.
// Gravity is restored when the set is empty.
// This is to prevent conflicts when gravity is overridden by more than one feature at the same time.
// @private
// @type {Set}
this._overriddenGravityRegister = new Set();
// Ensure selection is correct after each operation.
this.listenTo( this._model, 'applyOperation', ( evt, args ) => {
const operation = args[ 0 ];
if ( !operation.isDocumentOperation || operation.type == 'marker' || operation.type == 'rename' || operation.type == 'noop' ) {
return;
}
// Fix selection if the last range was removed from it and we have a position to which we can restore the selection.
if ( this._ranges.length == 0 && this._selectionRestorePosition ) {
this._fixGraveyardSelection( this._selectionRestorePosition );
}
// "Forget" the restore position even if it was not "used".
this._selectionRestorePosition = null;
if ( this._hasChangedRange ) {
this._hasChangedRange = false;
this.fire( 'change:range', { directChange: false } );
}
}, { priority: 'lowest' } );
// Ensure selection is correct and up to date after each range change.
this.on( 'change:range', () => {
for ( const range of this.getRanges() ) {
if ( !this._document._validateSelectionRange( range ) ) {
/**
* Range from {@link module:engine/model/documentselection~DocumentSelection document selection}
* starts or ends at incorrect position.
*
* @error document-selection-wrong-position
* @param {module:engine/model/range~Range} range
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_8__["default"](
'document-selection-wrong-position',
this,
{ range }
);
}
}
} );
// Update markers data stored by the selection after each marker change.
this.listenTo( this._model.markers, 'update', () => this._updateMarkers() );
// Ensure selection is up to date after each change block.
this.listenTo( this._document, 'change', ( evt, batch ) => {
clearAttributesStoredInElement( this._model, batch );
} );
}
get isCollapsed() {
const length = this._ranges.length;
return length === 0 ? this._document._getDefaultRange().isCollapsed : super.isCollapsed;
}
get anchor() {
return super.anchor || this._document._getDefaultRange().start;
}
get focus() {
return super.focus || this._document._getDefaultRange().end;
}
get rangeCount() {
return this._ranges.length ? this._ranges.length : 1;
}
// Describes whether `LiveSelection` has own range(s) set, or if it is defaulted to
// {@link module:engine/model/document~Document#_getDefaultRange document's default range}.
//
// @readonly
// @type {Boolean}
get hasOwnRange() {
return this._ranges.length > 0;
}
// When set to `true` then selection attributes on node before the caret won't be taken
// into consideration while updating selection attributes.
//
// @protected
// @type {Boolean}
get isGravityOverridden() {
return !!this._overriddenGravityRegister.size;
}
// Unbinds all events previously bound by live selection.
destroy() {
for ( let i = 0; i < this._ranges.length; i++ ) {
this._ranges[ i ].detach();
}
this.stopListening();
}
* getRanges() {
if ( this._ranges.length ) {
yield* super.getRanges();
} else {
yield this._document._getDefaultRange();
}
}
getFirstRange() {
return super.getFirstRange() || this._document._getDefaultRange();
}
getLastRange() {
return super.getLastRange() || this._document._getDefaultRange();
}
setTo( selectable, optionsOrPlaceOrOffset, options ) {
super.setTo( selectable, optionsOrPlaceOrOffset, options );
this._updateAttributes( true );
this._updateMarkers();
}
setFocus( itemOrPosition, offset ) {
super.setFocus( itemOrPosition, offset );
this._updateAttributes( true );
this._updateMarkers();
}
setAttribute( key, value ) {
if ( this._setAttribute( key, value ) ) {
// Fire event with exact data.
const attributeKeys = [ key ];
this.fire( 'change:attribute', { attributeKeys, directChange: true } );
}
}
removeAttribute( key ) {
if ( this._removeAttribute( key ) ) {
// Fire event with exact data.
const attributeKeys = [ key ];
this.fire( 'change:attribute', { attributeKeys, directChange: true } );
}
}
overrideGravity() {
const overrideUid = Object(_ckeditor_ckeditor5_utils_src_uid__WEBPACK_IMPORTED_MODULE_9__["default"])();
// Remember that another overriding has been requested. It will need to be removed
// before the gravity is to be restored.
this._overriddenGravityRegister.add( overrideUid );
if ( this._overriddenGravityRegister.size === 1 ) {
this._updateAttributes( true );
}
return overrideUid;
}
restoreGravity( uid ) {
if ( !this._overriddenGravityRegister.has( uid ) ) {
/**
* Restoring gravity for an unknown UID is not possible. Make sure you are using a correct
* UID obtained from the {@link module:engine/model/writer~Writer#overrideSelectionGravity} to restore.
*
* @error document-selection-gravity-wrong-restore
* @param {String} uid The unique identifier returned by
* {@link module:engine/model/documentselection~DocumentSelection#_overrideGravity}.
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_8__["default"](
'document-selection-gravity-wrong-restore',
this,
{ uid }
);
}
this._overriddenGravityRegister.delete( uid );
// Restore gravity only when all overriding have been restored.
if ( !this.isGravityOverridden ) {
this._updateAttributes( true );
}
}
_popRange() {
this._ranges.pop().detach();
}
_pushRange( range ) {
const liveRange = this._prepareRange( range );
// `undefined` is returned when given `range` is in graveyard root.
if ( liveRange ) {
this._ranges.push( liveRange );
}
}
// Prepares given range to be added to selection. Checks if it is correct,
// converts it to {@link module:engine/model/liverange~LiveRange LiveRange}
// and sets listeners listening to the range's change event.
//
// @private
// @param {module:engine/model/range~Range} range
_prepareRange( range ) {
this._checkRange( range );
if ( range.root == this._document.graveyard ) {
// @if CK_DEBUG // console.warn( 'Trying to add a Range that is in the graveyard root. Range rejected.' );
return;
}
const liveRange = _liverange__WEBPACK_IMPORTED_MODULE_3__["default"].fromRange( range );
// If selection range is moved to the graveyard remove it from the selection object.
// Also, save some data that can be used to restore selection later, on `Model#applyOperation` event.
liveRange.on( 'change:range', ( evt, oldRange, data ) => {
this._hasChangedRange = true;
if ( liveRange.root == this._document.graveyard ) {
this._selectionRestorePosition = data.deletionPosition;
const index = this._ranges.indexOf( liveRange );
this._ranges.splice( index, 1 );
liveRange.detach();
}
} );
return liveRange;
}
_updateMarkers() {
const markers = [];
let changed = false;
for ( const marker of this._model.markers ) {
const markerRange = marker.getRange();
for ( const selectionRange of this.getRanges() ) {
if ( markerRange.containsRange( selectionRange, !selectionRange.isCollapsed ) ) {
markers.push( marker );
}
}
}
const oldMarkers = Array.from( this.markers );
for ( const marker of markers ) {
if ( !this.markers.has( marker ) ) {
this.markers.add( marker );
changed = true;
}
}
for ( const marker of Array.from( this.markers ) ) {
if ( !markers.includes( marker ) ) {
this.markers.remove( marker );
changed = true;
}
}
if ( changed ) {
this.fire( 'change:marker', { oldMarkers, directChange: false } );
}
}
// Updates this selection attributes according to its ranges and the {@link module:engine/model/document~Document model document}.
//
// @protected
// @param {Boolean} clearAll
// @fires change:attribute
_updateAttributes( clearAll ) {
const newAttributes = Object(_ckeditor_ckeditor5_utils_src_tomap__WEBPACK_IMPORTED_MODULE_6__["default"])( this._getSurroundingAttributes() );
const oldAttributes = Object(_ckeditor_ckeditor5_utils_src_tomap__WEBPACK_IMPORTED_MODULE_6__["default"])( this.getAttributes() );
if ( clearAll ) {
// If `clearAll` remove all attributes and reset priorities.
this._attributePriority = new Map();
this._attrs = new Map();
} else {
// If not, remove only attributes added with `low` priority.
for ( const [ key, priority ] of this._attributePriority ) {
if ( priority == 'low' ) {
this._attrs.delete( key );
this._attributePriority.delete( key );
}
}
}
this._setAttributesTo( newAttributes );
// Let's evaluate which attributes really changed.
const changed = [];
// First, loop through all attributes that are set on selection right now.
// Check which of them are different than old attributes.
for ( const [ newKey, newValue ] of this.getAttributes() ) {
if ( !oldAttributes.has( newKey ) || oldAttributes.get( newKey ) !== newValue ) {
changed.push( newKey );
}
}
// Then, check which of old attributes got removed.
for ( const [ oldKey ] of oldAttributes ) {
if ( !this.hasAttribute( oldKey ) ) {
changed.push( oldKey );
}
}
// Fire event with exact data (fire only if anything changed).
if ( changed.length > 0 ) {
this.fire( 'change:attribute', { attributeKeys: changed, directChange: false } );
}
}
// Internal method for setting `LiveSelection` attribute. Supports attribute priorities (through `directChange`
// parameter).
//
// @private
// @param {String} key Attribute key.
// @param {*} value Attribute value.
// @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
// is caused by `Batch` API.
// @returns {Boolean} Whether value has changed.
_setAttribute( key, value, directChange = true ) {
const priority = directChange ? 'normal' : 'low';
if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
// Priority too low.
return false;
}
const oldValue = super.getAttribute( key );
// Don't do anything if value has not changed.
if ( oldValue === value ) {
return false;
}
this._attrs.set( key, value );
// Update priorities map.
this._attributePriority.set( key, priority );
return true;
}
// Internal method for removing `LiveSelection` attribute. Supports attribute priorities (through `directChange`
// parameter).
//
// NOTE: Even if attribute is not present in the selection but is provided to this method, it's priority will
// be changed according to `directChange` parameter.
//
// @private
// @param {String} key Attribute key.
// @param {Boolean} [directChange=true] `true` if the change is caused by `Selection` API, `false` if change
// is caused by `Batch` API.
// @returns {Boolean} Whether attribute was removed. May not be true if such attributes didn't exist or the
// existing attribute had higher priority.
_removeAttribute( key, directChange = true ) {
const priority = directChange ? 'normal' : 'low';
if ( priority == 'low' && this._attributePriority.get( key ) == 'normal' ) {
// Priority too low.
return false;
}
// Update priorities map.
this._attributePriority.set( key, priority );
// Don't do anything if value has not changed.
if ( !super.hasAttribute( key ) ) {
return false;
}
this._attrs.delete( key );
return true;
}
// Internal method for setting multiple `LiveSelection` attributes. Supports attribute priorities (through
// `directChange` parameter).
//
// @private
// @param {Map.} attrs Iterable object containing attributes to be set.
// @returns {Set.} Changed attribute keys.
_setAttributesTo( attrs ) {
const changed = new Set();
for ( const [ oldKey, oldValue ] of this.getAttributes() ) {
// Do not remove attribute if attribute with same key and value is about to be set.
if ( attrs.get( oldKey ) === oldValue ) {
continue;
}
// All rest attributes will be removed so changed attributes won't change .
this._removeAttribute( oldKey, false );
}
for ( const [ key, value ] of attrs ) {
// Attribute may not be set because of attributes or because same key/value is already added.
const gotAdded = this._setAttribute( key, value, false );
if ( gotAdded ) {
changed.add( key );
}
}
return changed;
}
// Returns an iterable that iterates through all selection attributes stored in current selection's parent.
//
// @protected
// @returns {Iterable.<*>}
* _getStoredAttributes() {
const selectionParent = this.getFirstPosition().parent;
if ( this.isCollapsed && selectionParent.isEmpty ) {
for ( const key of selectionParent.getAttributeKeys() ) {
if ( key.startsWith( storePrefix ) ) {
const realKey = key.substr( storePrefix.length );
yield [ realKey, selectionParent.getAttribute( key ) ];
}
}
}
}
// Checks model text nodes that are closest to the selection's first position and returns attributes of first
// found element. If there are no text nodes in selection's first position parent, it returns selection
// attributes stored in that parent.
//
// @private
// @returns {Iterable.<*>} Collection of attributes.
_getSurroundingAttributes() {
const position = this.getFirstPosition();
const schema = this._model.schema;
let attrs = null;
if ( !this.isCollapsed ) {
// 1. If selection is a range...
const range = this.getFirstRange();
// ...look for a first character node in that range and take attributes from it.
for ( const value of range ) {
// If the item is an object, we don't want to get attributes from its children.
if ( value.item.is( 'element' ) && schema.isObject( value.item ) ) {
break;
}
if ( value.type == 'text' ) {
attrs = value.item.getAttributes();
break;
}
}
} else {
// 2. If the selection is a caret or the range does not contain a character node...
const nodeBefore = position.textNode ? position.textNode : position.nodeBefore;
const nodeAfter = position.textNode ? position.textNode : position.nodeAfter;
// When gravity is overridden then don't take node before into consideration.
if ( !this.isGravityOverridden ) {
// ...look at the node before caret and take attributes from it if it is a character node.
attrs = getAttrsIfCharacter( nodeBefore );
}
// 3. If not, look at the node after caret...
if ( !attrs ) {
attrs = getAttrsIfCharacter( nodeAfter );
}
// 4. If not, try to find the first character on the left, that is in the same node.
// When gravity is overridden then don't take node before into consideration.
if ( !this.isGravityOverridden && !attrs ) {
let node = nodeBefore;
while ( node && !schema.isInline( node ) && !attrs ) {
node = node.previousSibling;
attrs = getAttrsIfCharacter( node );
}
}
// 5. If not found, try to find the first character on the right, that is in the same node.
if ( !attrs ) {
let node = nodeAfter;
while ( node && !schema.isInline( node ) && !attrs ) {
node = node.nextSibling;
attrs = getAttrsIfCharacter( node );
}
}
// 6. If not found, selection should retrieve attributes from parent.
if ( !attrs ) {
attrs = this._getStoredAttributes();
}
}
return attrs;
}
// Fixes the selection after all its ranges got removed.
//
// @private
// @param {module:engine/model/position~Position} deletionPosition Position where the deletion happened.
_fixGraveyardSelection( deletionPosition ) {
// Find a range that is a correct selection range and is closest to the position where the deletion happened.
const selectionRange = this._model.schema.getNearestSelectionRange( deletionPosition );
// If nearest valid selection range has been found - add it in the place of old range.
if ( selectionRange ) {
// Check the range, convert it to live range, bind events, etc.
this._pushRange( selectionRange );
}
// If nearest valid selection range cannot be found don't add any range. Selection will be set to the default range.
}
}
// Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
//
// It takes model item, checks whether it is a text node (or text proxy) and, if so, returns it's attributes. If not, returns `null`.
//
// @param {module:engine/model/item~Item|null} node
// @returns {Boolean}
function getAttrsIfCharacter( node ) {
if ( node instanceof _textproxy__WEBPACK_IMPORTED_MODULE_5__["default"] || node instanceof _text__WEBPACK_IMPORTED_MODULE_4__["default"] ) {
return node.getAttributes();
}
return null;
}
// Removes selection attributes from element which is not empty anymore.
//
// @param {module:engine/model/model~Model} model
// @param {module:engine/model/batch~Batch} batch
function clearAttributesStoredInElement( model, batch ) {
const differ = model.document.differ;
for ( const entry of differ.getChanges() ) {
if ( entry.type != 'insert' ) {
continue;
}
const changeParent = entry.position.parent;
const isNoLongerEmpty = entry.length === changeParent.maxOffset;
if ( isNoLongerEmpty ) {
model.enqueueChange( batch, writer => {
const storedAttributes = Array.from( changeParent.getAttributeKeys() )
.filter( key => key.startsWith( storePrefix ) );
for ( const key of storedAttributes ) {
writer.removeAttribute( key, changeParent );
}
} );
}
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js":
/*!**********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js ***!
\**********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Element; });
/* harmony import */ var _node__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js");
/* harmony import */ var _nodelist__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./nodelist */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js");
/* harmony import */ var _text__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./text */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js");
/* harmony import */ var _textproxy__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./textproxy */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/textproxy.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_isiterable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/isiterable */ "./node_modules/@ckeditor/ckeditor5-utils/src/isiterable.js");
/**
* @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 engine/model/element
*/
// @if CK_DEBUG_ENGINE // const { stringifyMap, convertMapToStringifiedObject, convertMapToTags } = require( '../dev-utils/utils' );
/**
* Model element. Type of {@link module:engine/model/node~Node node} that has a {@link module:engine/model/element~Element#name name} and
* {@link module:engine/model/element~Element#getChildren child nodes}.
*
* **Important**: see {@link module:engine/model/node~Node} to read about restrictions using `Element` and `Node` API.
*
* @extends module:engine/model/node~Node
*/
class Element extends _node__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates a model element.
*
* **Note:** Constructor of this class shouldn't be used directly in the code.
* Use the {@link module:engine/model/writer~Writer#createElement} method instead.
*
* @protected
* @param {String} name Element's name.
* @param {Object} [attrs] Element's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
* @param {module:engine/model/node~Node|Iterable.} [children]
* One or more nodes to be inserted as children of created element.
*/
constructor( name, attrs, children ) {
super( attrs );
/**
* Element name.
*
* @readonly
* @member {String} module:engine/model/element~Element#name
*/
this.name = name;
/**
* List of children nodes.
*
* @private
* @member {module:engine/model/nodelist~NodeList} module:engine/model/element~Element#_children
*/
this._children = new _nodelist__WEBPACK_IMPORTED_MODULE_1__["default"]();
if ( children ) {
this._insertChild( 0, children );
}
}
/**
* Number of this element's children.
*
* @readonly
* @type {Number}
*/
get childCount() {
return this._children.length;
}
/**
* Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all of this element's children.
*
* @readonly
* @type {Number}
*/
get maxOffset() {
return this._children.maxOffset;
}
/**
* Is `true` if there are no nodes inside this element, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isEmpty() {
return this.childCount === 0;
}
/**
* Checks whether this object is of the given.
*
* element.is( 'element' ); // -> true
* element.is( 'node' ); // -> true
* element.is( 'model:element' ); // -> true
* element.is( 'model:node' ); // -> true
*
* element.is( 'view:element' ); // -> false
* element.is( 'documentSelection' ); // -> false
*
* Assuming that the object being checked is an element, you can also check its
* {@link module:engine/model/element~Element#name name}:
*
* element.is( 'element', 'image' ); // -> true if this is an element
* element.is( 'element', 'image' ); // -> same as above
* text.is( 'element', 'image' ); -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type Type to check.
* @param {String} [name] Element name.
* @returns {Boolean}
*/
is( type, name = null ) {
if ( !name ) {
return type === 'element' || type === 'model:element' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === 'node' || type === 'model:node';
}
return name === this.name && ( type === 'element' || type === 'model:element' );
}
/**
* Gets the child at the given index.
*
* @param {Number} index Index of child.
* @returns {module:engine/model/node~Node} Child node.
*/
getChild( index ) {
return this._children.getNode( index );
}
/**
* Returns an iterator that iterates over all of this element's children.
*
* @returns {Iterable.}
*/
getChildren() {
return this._children[ Symbol.iterator ]();
}
/**
* Returns an index of the given child node. Returns `null` if given node is not a child of this element.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number} Child node's index in this element.
*/
getChildIndex( node ) {
return this._children.getNodeIndex( node );
}
/**
* Returns the starting offset of given child. Starting offset is equal to the sum of
* {@link module:engine/model/node~Node#offsetSize offset sizes} of all node's siblings that are before it. Returns `null` if
* given node is not a child of this element.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number} Child node's starting offset.
*/
getChildStartOffset( node ) {
return this._children.getNodeStartOffset( node );
}
/**
* Returns index of a node that occupies given offset. If given offset is too low, returns `0`. If given offset is
* too high, returns {@link module:engine/model/element~Element#getChildIndex index after last child}.
*
* const textNode = new Text( 'foo' );
* const pElement = new Element( 'p' );
* const divElement = new Element( [ textNode, pElement ] );
* divElement.offsetToIndex( -1 ); // Returns 0, because offset is too low.
* divElement.offsetToIndex( 0 ); // Returns 0, because offset 0 is taken by `textNode` which is at index 0.
* divElement.offsetToIndex( 1 ); // Returns 0, because `textNode` has `offsetSize` equal to 3, so it occupies offset 1 too.
* divElement.offsetToIndex( 2 ); // Returns 0.
* divElement.offsetToIndex( 3 ); // Returns 1.
* divElement.offsetToIndex( 4 ); // Returns 2. There are no nodes at offset 4, so last available index is returned.
*
* @param {Number} offset Offset to look for.
* @returns {Number}
*/
offsetToIndex( offset ) {
return this._children.offsetToIndex( offset );
}
/**
* Returns a descendant node by its path relative to this element.
*
* // ac
* this.getNodeByPath( [ 0 ] ); // -> "a"
* this.getNodeByPath( [ 1 ] ); // ->
* this.getNodeByPath( [ 1, 0 ] ); // -> "c"
*
* @param {Array.} relativePath Path of the node to find, relative to this element.
* @returns {module:engine/model/node~Node}
*/
getNodeByPath( relativePath ) {
let node = this; // eslint-disable-line consistent-this
for ( const index of relativePath ) {
node = node.getChild( node.offsetToIndex( index ) );
}
return node;
}
/**
* Returns the parent element of the given name. Returns null if the element is not inside the desired parent.
*
* @param {String} parentName The name of the parent element to find.
* @param {Object} [options] Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included while searching.
* @returns {module:engine/model/element~Element|null}
*/
findAncestor( parentName, options = { includeSelf: false } ) {
let parent = options.includeSelf ? this : this.parent;
while ( parent ) {
if ( parent.name === parentName ) {
return parent;
}
parent = parent.parent;
}
return null;
}
/**
* Converts `Element` instance to plain object and returns it. Takes care of converting all of this element's children.
*
* @returns {Object} `Element` instance converted to plain object.
*/
toJSON() {
const json = super.toJSON();
json.name = this.name;
if ( this._children.length > 0 ) {
json.children = [];
for ( const node of this._children ) {
json.children.push( node.toJSON() );
}
}
return json;
}
/**
* 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 returned.
*
* @protected
* @param {Boolean} [deep=false] If set to `true` clones element and all its children recursively. When set to `false`,
* element will be cloned without any child.
*/
_clone( deep = false ) {
const children = deep ? Array.from( this._children ).map( node => node._clone( true ) ) : null;
return new Element( this.name, this.getAttributes(), children );
}
/**
* {@link module:engine/model/element~Element#_insertChild Inserts} one or more nodes at the end of this element.
*
* @see module:engine/model/writer~Writer#append
* @protected
* @param {module:engine/model/item~Item|Iterable.} nodes Nodes to be inserted.
*/
_appendChild( nodes ) {
this._insertChild( this.childCount, nodes );
}
/**
* Inserts one or more nodes at the given index and sets {@link module:engine/model/node~Node#parent parent} of these nodes
* to this element.
*
* @see module:engine/model/writer~Writer#insert
* @protected
* @param {Number} index Index at which nodes should be inserted.
* @param {module:engine/model/item~Item|Iterable.} items Items to be inserted.
*/
_insertChild( index, items ) {
const nodes = normalize( items );
for ( const node of nodes ) {
// If node that is being added to this element is already inside another element, first remove it from the old parent.
if ( node.parent !== null ) {
node._remove();
}
node.parent = this;
}
this._children._insertNodes( index, nodes );
}
/**
* Removes one or more nodes starting at the given index and sets
* {@link module:engine/model/node~Node#parent parent} of these nodes to `null`.
*
* @see module:engine/model/writer~Writer#remove
* @protected
* @param {Number} index Index of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.} Array containing removed nodes.
*/
_removeChildren( index, howMany = 1 ) {
const nodes = this._children._removeNodes( index, howMany );
for ( const node of nodes ) {
node.parent = null;
}
return nodes;
}
/**
* Creates an `Element` instance from given plain object (i.e. parsed JSON string).
* Converts `Element` children to proper nodes.
*
* @param {Object} json Plain object to be converted to `Element`.
* @returns {module:engine/model/element~Element} `Element` instance created using given plain object.
*/
static fromJSON( json ) {
let children = null;
if ( json.children ) {
children = [];
for ( const child of json.children ) {
if ( child.name ) {
// If child has name property, it is an Element.
children.push( Element.fromJSON( child ) );
} else {
// Otherwise, it is a Text node.
children.push( _text__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( child ) );
}
}
}
return new Element( json.name, json.attributes, children );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `<${ this.rootName || this.name }>`;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // log() {
// @if CK_DEBUG_ENGINE // console.log( 'ModelElement: ' + this );
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // logExtended() {
// @if CK_DEBUG_ENGINE // console.log( `ModelElement: ${ this }, ${ this.childCount } children,
// @if CK_DEBUG_ENGINE // attrs: ${ convertMapToStringifiedObject( this.getAttributes() ) }` );
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // logAll() {
// @if CK_DEBUG_ENGINE // console.log( '--------------------' );
// @if CK_DEBUG_ENGINE //
// @if CK_DEBUG_ENGINE // this.logExtended();
// @if CK_DEBUG_ENGINE // console.log( 'List of children:' );
// @if CK_DEBUG_ENGINE //
// @if CK_DEBUG_ENGINE // for ( const child of this.getChildren() ) {
// @if CK_DEBUG_ENGINE // child.log();
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // printTree( level = 0) {
// @if CK_DEBUG_ENGINE // let string = '';
// @if CK_DEBUG_ENGINE // string += '\t'.repeat( level );
// @if CK_DEBUG_ENGINE // string += `<${ this.rootName || this.name }${ convertMapToTags( this.getAttributes() ) }>`;
// @if CK_DEBUG_ENGINE // for ( const child of this.getChildren() ) {
// @if CK_DEBUG_ENGINE // string += '\n';
// @if CK_DEBUG_ENGINE // if ( child.is( '$text' ) ) {
// @if CK_DEBUG_ENGINE // const textAttrs = convertMapToTags( child._attrs );
// @if CK_DEBUG_ENGINE // string += '\t'.repeat( level + 1 );
// @if CK_DEBUG_ENGINE // if ( textAttrs !== '' ) {
// @if CK_DEBUG_ENGINE // string += `<$text${ textAttrs }>` + child.data + '$text>';
// @if CK_DEBUG_ENGINE // } else {
// @if CK_DEBUG_ENGINE // string += child.data;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // } else {
// @if CK_DEBUG_ENGINE // string += child.printTree( level + 1 );
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // if ( this.childCount ) {
// @if CK_DEBUG_ENGINE // string += '\n' + '\t'.repeat( level );
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // string += `${ this.rootName || this.name }>`;
// @if CK_DEBUG_ENGINE // return string;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // logTree() {
// @if CK_DEBUG_ENGINE // console.log( this.printTree() );
// @if CK_DEBUG_ENGINE // }
}
// Converts strings to Text and non-iterables to arrays.
//
// @param {String|module:engine/model/item~Item|Iterable.}
// @returns {Iterable.}
function normalize( nodes ) {
// Separate condition because string is iterable.
if ( typeof nodes == 'string' ) {
return [ new _text__WEBPACK_IMPORTED_MODULE_2__["default"]( nodes ) ];
}
if ( !Object(_ckeditor_ckeditor5_utils_src_isiterable__WEBPACK_IMPORTED_MODULE_4__["default"])( nodes ) ) {
nodes = [ nodes ];
}
// Array.from to enable .map() on non-arrays.
return Array.from( nodes )
.map( node => {
if ( typeof node == 'string' ) {
return new _text__WEBPACK_IMPORTED_MODULE_2__["default"]( node );
}
if ( node instanceof _textproxy__WEBPACK_IMPORTED_MODULE_3__["default"] ) {
return new _text__WEBPACK_IMPORTED_MODULE_2__["default"]( node.data, node.getAttributes() );
}
return node;
} );
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/history.js":
/*!**********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/history.js ***!
\**********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return History; });
/**
* @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 engine/model/history
*/
/**
* `History` keeps the track of all the operations applied to the {@link module:engine/model/document~Document document}.
*/
class History {
/**
* Creates an empty History instance.
*/
constructor() {
/**
* Operations added to the history.
*
* @protected
* @member {Array.} module:engine/model/history~History#_operations
*/
this._operations = [];
/**
* Holds an information which {@link module:engine/model/operation/operation~Operation operation} undoes which
* {@link module:engine/model/operation/operation~Operation operation}.
*
* Keys of the map are "undoing operations", that is operations that undone some other operations. For each key, the
* value is an operation that has been undone by the "undoing operation".
*
* @private
* @member {Map} module:engine/model/history~History#_undoPairs
*/
this._undoPairs = new Map();
/**
* Holds all undone operations.
*
* @private
* @member {Set.} module:engine/model/history~History#_undoneOperations
*/
this._undoneOperations = new Set();
}
/**
* Adds an operation to the history.
*
* @param {module:engine/model/operation/operation~Operation} operation Operation to add.
*/
addOperation( operation ) {
if ( this._operations.includes( operation ) ) {
return;
}
this._operations.push( operation );
}
/**
* Returns operations added to the history.
*
* @param {Number} [from=Number.NEGATIVE_INFINITY] Base version from which operations should be returned (inclusive).
* Defaults to `Number.NEGATIVE_INFINITY`, which means that operations from the first one will be returned.
* @param {Number} [to=Number.POSITIVE_INFINITY] Base version up to which operations should be returned (exclusive).
* Defaults to `Number.POSITIVE_INFINITY` which means that operations up to the last one will be returned.
* @returns {Array.} Operations added to the history.
*/
getOperations( from = Number.NEGATIVE_INFINITY, to = Number.POSITIVE_INFINITY ) {
const operations = [];
for ( const operation of this._operations ) {
if ( operation.baseVersion >= from && operation.baseVersion < to ) {
operations.push( operation );
}
}
return operations;
}
/**
* Returns operation from the history that bases on given `baseVersion`.
*
* @param {Number} baseVersion Base version of the operation to get.
* @returns {module:engine/model/operation/operation~Operation|undefined} Operation with given base version or `undefined` if
* there is no such operation in history.
*/
getOperation( baseVersion ) {
for ( const operation of this._operations ) {
if ( operation.baseVersion == baseVersion ) {
return operation;
}
}
}
/**
* Marks in history that one operation is an operation that is undoing the other operation. By marking operation this way,
* history is keeping more context information about operations, which helps in operational transformation.
*
* @param {module:engine/model/operation/operation~Operation} undoneOperation Operation which is undone by `undoingOperation`.
* @param {module:engine/model/operation/operation~Operation} undoingOperation Operation which undoes `undoneOperation`.
*/
setOperationAsUndone( undoneOperation, undoingOperation ) {
this._undoPairs.set( undoingOperation, undoneOperation );
this._undoneOperations.add( undoneOperation );
}
/**
* Checks whether given `operation` is undoing any other operation.
*
* @param {module:engine/model/operation/operation~Operation} operation Operation to check.
* @returns {Boolean} `true` if given `operation` is undoing any other operation, `false` otherwise.
*/
isUndoingOperation( operation ) {
return this._undoPairs.has( operation );
}
/**
* Checks whether given `operation` has been undone by any other operation.
*
* @param {module:engine/model/operation/operation~Operation} operation Operation to check.
* @returns {Boolean} `true` if given `operation` has been undone any other operation, `false` otherwise.
*/
isUndoneOperation( operation ) {
return this._undoneOperations.has( operation );
}
/**
* For given `undoingOperation`, returns the operation which has been undone by it.
*
* @param {module:engine/model/operation/operation~Operation} undoingOperation
* @returns {module:engine/model/operation/operation~Operation|undefined} Operation that has been undone by given
* `undoingOperation` or `undefined` if given `undoingOperation` is not undoing any other operation.
*/
getUndoneOperation( undoingOperation ) {
return this._undoPairs.get( undoingOperation );
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/liveposition.js":
/*!***************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/liveposition.js ***!
\***************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return LivePosition; });
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/emittermixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/liveposition
*/
/**
* `LivePosition` is a type of {@link module:engine/model/position~Position Position}
* that updates itself as {@link module:engine/model/document~Document document}
* is changed through operations. It may be used as a bookmark.
*
* **Note:** Contrary to {@link module:engine/model/position~Position}, `LivePosition` works only in roots that are
* {@link module:engine/model/rootelement~RootElement}.
* If {@link module:engine/model/documentfragment~DocumentFragment} is passed, error will be thrown.
*
* **Note:** Be very careful when dealing with `LivePosition`. Each `LivePosition` instance bind events that might
* have to be unbound.
* Use {@link module:engine/model/liveposition~LivePosition#detach} whenever you don't need `LivePosition` anymore.
*
* @extends module:engine/model/position~Position
*/
class LivePosition extends _position__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates a live position.
*
* @see module:engine/model/position~Position
* @param {module:engine/model/rootelement~RootElement} root
* @param {Array.} path
* @param {module:engine/model/position~PositionStickiness} [stickiness]
*/
constructor( root, path, stickiness = 'toNone' ) {
super( root, path, stickiness );
if ( !this.root.is( 'rootElement' ) ) {
/**
* LivePosition's root has to be an instance of RootElement.
*
* @error model-liveposition-root-not-rootelement
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__["default"]( 'model-liveposition-root-not-rootelement', root );
}
bindWithDocument.call( this );
}
/**
* Unbinds all events previously bound by `LivePosition`. Use it whenever you don't need `LivePosition` instance
* anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
* referring to it).
*/
detach() {
this.stopListening();
}
/**
* Checks whether this object is of the given.
*
* livePosition.is( 'position' ); // -> true
* livePosition.is( 'model:position' ); // -> true
* livePosition.is( 'liveposition' ); // -> true
* livePosition.is( 'model:livePosition' ); // -> true
*
* livePosition.is( 'view:position' ); // -> false
* livePosition.is( 'documentSelection' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'livePosition' || type === 'model:livePosition' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type == 'position' || type === 'model:position';
}
/**
* Creates a {@link module:engine/model/position~Position position instance}, which is equal to this live position.
*
* @returns {module:engine/model/position~Position}
*/
toPosition() {
return new _position__WEBPACK_IMPORTED_MODULE_0__["default"]( this.root, this.path.slice(), this.stickiness );
}
/**
* Creates a `LivePosition` instance that is equal to position.
*
* @param {module:engine/model/position~Position} position
* @param {module:engine/model/position~PositionStickiness} [stickiness]
* @returns {module:engine/model/position~Position}
*/
static fromPosition( position, stickiness ) {
return new this( position.root, position.path.slice(), stickiness ? stickiness : position.stickiness );
}
/**
* @static
* @protected
* @method module:engine/model/liveposition~LivePosition._createAfter
* @see module:engine/model/position~Position._createAfter
* @param {module:engine/model/node~Node} node
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
* @returns {module:engine/model/liveposition~LivePosition}
*/
/**
* @static
* @protected
* @method module:engine/model/liveposition~LivePosition._createBefore
* @see module:engine/model/position~Position._createBefore
* @param {module:engine/model/node~Node} node
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
* @returns {module:engine/model/liveposition~LivePosition}
*/
/**
* @static
* @protected
* @method module:engine/model/liveposition~LivePosition._createAt
* @see module:engine/model/position~Position._createAt
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset]
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
* @returns {module:engine/model/liveposition~LivePosition}
*/
/**
* Fired when `LivePosition` instance is changed due to changes on {@link module:engine/model/document~Document}.
*
* @event module:engine/model/liveposition~LivePosition#change
* @param {module:engine/model/position~Position} oldPosition Position equal to this live position before it got changed.
*/
}
// Binds this `LivePosition` to the {@link module:engine/model/document~Document document} that owns
// this position's {@link module:engine/model/position~Position#root root}.
//
// @private
function bindWithDocument() {
this.listenTo(
this.root.document.model,
'applyOperation',
( event, args ) => {
const operation = args[ 0 ];
if ( !operation.isDocumentOperation ) {
return;
}
transform.call( this, operation );
},
{ priority: 'low' }
);
}
// Updates this position accordingly to the updates applied to the model. Bases on change events.
//
// @private
// @param {module:engine/model/operation/operation~Operation} operation Executed operation.
function transform( operation ) {
const result = this.getTransformedByOperation( operation );
if ( !this.isEqual( result ) ) {
const oldPosition = this.toPosition();
this.path = result.path;
this.root = result.root;
this.fire( 'change', oldPosition );
}
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_2__["default"])( LivePosition, _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__["default"] );
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/liverange.js":
/*!************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/liverange.js ***!
\************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return LiveRange; });
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/emittermixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/**
* @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 engine/model/liverange
*/
/**
* `LiveRange` is a type of {@link module:engine/model/range~Range Range}
* that updates itself as {@link module:engine/model/document~Document document}
* is changed through operations. It may be used as a bookmark.
*
* **Note:** Be very careful when dealing with `LiveRange`. Each `LiveRange` instance bind events that might
* have to be unbound. Use {@link module:engine/model/liverange~LiveRange#detach detach} whenever you don't need `LiveRange` anymore.
*/
class LiveRange extends _range__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates a live range.
*
* @see module:engine/model/range~Range
*/
constructor( start, end ) {
super( start, end );
bindWithDocument.call( this );
}
/**
* Unbinds all events previously bound by `LiveRange`. Use it whenever you don't need `LiveRange` instance
* anymore (i.e. when leaving scope in which it was declared or before re-assigning variable that was
* referring to it).
*/
detach() {
this.stopListening();
}
/**
* Checks whether this object is of the given.
*
* liveRange.is( 'range' ); // -> true
* liveRange.is( 'model:range' ); // -> true
* liveRange.is( 'liveRange' ); // -> true
* liveRange.is( 'model:liveRange' ); // -> true
*
* liveRange.is( 'view:range' ); // -> false
* liveRange.is( 'documentSelection' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'liveRange' || type === 'model:liveRange' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type == 'range' || type === 'model:range';
}
/**
* Creates a {@link module:engine/model/range~Range range instance} that is equal to this live range.
*
* @returns {module:engine/model/range~Range}
*/
toRange() {
return new _range__WEBPACK_IMPORTED_MODULE_0__["default"]( this.start, this.end );
}
/**
* Creates a `LiveRange` instance that is equal to the given range.
*
* @param {module:engine/model/range~Range} range
* @returns {module:engine/model/liverange~LiveRange}
*/
static fromRange( range ) {
return new LiveRange( range.start, range.end );
}
/**
* @see module:engine/model/range~Range._createIn
* @static
* @protected
* @method module:engine/model/liverange~LiveRange._createIn
* @param {module:engine/model/element~Element} element
* @returns {module:engine/model/liverange~LiveRange}
*/
/**
* @see module:engine/model/range~Range._createOn
* @static
* @protected
* @method module:engine/model/liverange~LiveRange._createOn
* @param {module:engine/model/element~Element} element
* @returns {module:engine/model/liverange~LiveRange}
*/
/**
* @see module:engine/model/range~Range._createFromPositionAndShift
* @static
* @protected
* @method module:engine/model/liverange~LiveRange._createFromPositionAndShift
* @param {module:engine/model/position~Position} position
* @param {Number} shift
* @returns {module:engine/model/liverange~LiveRange}
*/
/**
* Fired when `LiveRange` instance boundaries have changed due to changes in the
* {@link module:engine/model/document~Document document}.
*
* @event change:range
* @param {module:engine/model/range~Range} oldRange Range with start and end position equal to start and end position of this live
* range before it got changed.
* @param {Object} data Object with additional information about the change.
* @param {module:engine/model/position~Position|null} data.deletionPosition Source position for remove and merge changes.
* Available if the range was moved to the graveyard root, `null` otherwise.
*/
/**
* Fired when `LiveRange` instance boundaries have not changed after a change in {@link module:engine/model/document~Document document}
* but the change took place inside the range, effectively changing its content.
*
* @event change:content
* @param {module:engine/model/range~Range} range Range with start and end position equal to start and end position of
* change range.
* @param {Object} data Object with additional information about the change.
* @param {null} data.deletionPosition Due to the nature of this event, this property is always set to `null`. It is passed
* for compatibility with the {@link module:engine/model/liverange~LiveRange#event:change:range} event.
*/
}
// Binds this `LiveRange` to the {@link module:engine/model/document~Document document}
// that owns this range's {@link module:engine/model/range~Range#root root}.
//
// @private
function bindWithDocument() {
this.listenTo(
this.root.document.model,
'applyOperation',
( event, args ) => {
const operation = args[ 0 ];
if ( !operation.isDocumentOperation ) {
return;
}
transform.call( this, operation );
},
{ priority: 'low' }
);
}
// Updates this range accordingly to the updates applied to the model. Bases on change events.
//
// @private
// @param {module:engine/model/operation/operation~Operation} operation Executed operation.
function transform( operation ) {
// Transform the range by the operation. Join the result ranges if needed.
const ranges = this.getTransformedByOperation( operation );
const result = _range__WEBPACK_IMPORTED_MODULE_0__["default"]._createFromRanges( ranges );
const boundariesChanged = !result.isEqual( this );
const contentChanged = doesOperationChangeRangeContent( this, operation );
let deletionPosition = null;
if ( boundariesChanged ) {
// If range boundaries have changed, fire `change:range` event.
//
if ( result.root.rootName == '$graveyard' ) {
// If the range was moved to the graveyard root, set `deletionPosition`.
if ( operation.type == 'remove' ) {
deletionPosition = operation.sourcePosition;
} else {
// Merge operation.
deletionPosition = operation.deletionPosition;
}
}
const oldRange = this.toRange();
this.start = result.start;
this.end = result.end;
this.fire( 'change:range', oldRange, { deletionPosition } );
} else if ( contentChanged ) {
// If range boundaries have not changed, but there was change inside the range, fire `change:content` event.
this.fire( 'change:content', this.toRange(), { deletionPosition } );
}
}
// Checks whether given operation changes something inside the range (even if it does not change boundaries).
//
// @private
// @param {module:engine/model/range~Range} range Range to check.
// @param {module:engine/model/operation/operation~Operation} operation Executed operation.
// @returns {Boolean}
function doesOperationChangeRangeContent( range, operation ) {
switch ( operation.type ) {
case 'insert':
return range.containsPosition( operation.position );
case 'move':
case 'remove':
case 'reinsert':
case 'merge':
return range.containsPosition( operation.sourcePosition ) ||
range.start.isEqual( operation.sourcePosition ) ||
range.containsPosition( operation.targetPosition );
case 'split':
return range.containsPosition( operation.splitPosition ) || range.containsPosition( operation.insertionPosition );
}
return false;
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_2__["default"])( LiveRange, _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__["default"] );
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/markercollection.js":
/*!*******************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/markercollection.js ***!
\*******************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return MarkerCollection; });
/* harmony import */ var _liverange__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./liverange */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/liverange.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/emittermixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/**
* @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 engine/model/markercollection
*/
/**
* The collection of all {@link module:engine/model/markercollection~Marker markers} attached to the document.
* It lets you {@link module:engine/model/markercollection~MarkerCollection#get get} markers or track them using
* {@link module:engine/model/markercollection~MarkerCollection#event:update} event.
*
* To create, change or remove makers use {@link module:engine/model/writer~Writer model writers'} methods:
* {@link module:engine/model/writer~Writer#addMarker} or {@link module:engine/model/writer~Writer#removeMarker}. Since
* the writer is the only proper way to change the data model it is not possible to change markers directly using this
* collection. All markers created by the writer will be automatically added to this collection.
*
* By default there is one marker collection available as {@link module:engine/model/model~Model#markers model property}.
*
* @see module:engine/model/markercollection~Marker
*/
class MarkerCollection {
/**
* Creates a markers collection.
*/
constructor() {
/**
* Stores {@link ~Marker markers} added to the collection.
*
* @private
* @member {Map} #_markers
*/
this._markers = new Map();
}
/**
* Iterable interface.
*
* Iterates over all {@link ~Marker markers} added to the collection.
*
* @returns {Iterable}
*/
[ Symbol.iterator ]() {
return this._markers.values();
}
/**
* Checks if marker with given `markerName` is in the collection.
*
* @param {String} markerName Marker name.
* @returns {Boolean} `true` if marker with given `markerName` is in the collection, `false` otherwise.
*/
has( markerName ) {
return this._markers.has( markerName );
}
/**
* Returns {@link ~Marker marker} with given `markerName`.
*
* @param {String} markerName Name of marker to get.
* @returns {module:engine/model/markercollection~Marker|null} Marker with given name or `null` if such marker was
* not added to the collection.
*/
get( markerName ) {
return this._markers.get( markerName ) || null;
}
/**
* Creates and adds a {@link ~Marker marker} to the `MarkerCollection` with given name on given
* {@link module:engine/model/range~Range range}.
*
* If `MarkerCollection` already had a marker with given name (or {@link ~Marker marker} was passed), the marker in
* collection is updated and {@link module:engine/model/markercollection~MarkerCollection#event:update} event is fired
* but only if there was a change (marker range or {@link module:engine/model/markercollection~Marker#managedUsingOperations}
* flag has changed.
*
* @protected
* @fires module:engine/model/markercollection~MarkerCollection#event:update
* @param {String|module:engine/model/markercollection~Marker} markerOrName Name of marker to set or marker instance to update.
* @param {module:engine/model/range~Range} range Marker range.
* @param {Boolean} [managedUsingOperations=false] Specifies whether the marker is managed using operations.
* @param {Boolean} [affectsData=false] Specifies whether the marker affects the data produced by the data pipeline
* (is persisted in the editor's data).
* @returns {module:engine/model/markercollection~Marker} `Marker` instance which was added or updated.
*/
_set( markerOrName, range, managedUsingOperations = false, affectsData = false ) {
const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
if ( markerName.includes( ',' ) ) {
/**
* Marker name cannot contain the "," character.
*
* @error markercollection-incorrect-marker-name
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'markercollection-incorrect-marker-name', this );
}
const oldMarker = this._markers.get( markerName );
if ( oldMarker ) {
const oldRange = oldMarker.getRange();
let hasChanged = false;
if ( !oldRange.isEqual( range ) ) {
oldMarker._attachLiveRange( _liverange__WEBPACK_IMPORTED_MODULE_0__["default"].fromRange( range ) );
hasChanged = true;
}
if ( managedUsingOperations != oldMarker.managedUsingOperations ) {
oldMarker._managedUsingOperations = managedUsingOperations;
hasChanged = true;
}
if ( typeof affectsData === 'boolean' && affectsData != oldMarker.affectsData ) {
oldMarker._affectsData = affectsData;
hasChanged = true;
}
if ( hasChanged ) {
this.fire( 'update:' + markerName, oldMarker, oldRange, range );
}
return oldMarker;
}
const liveRange = _liverange__WEBPACK_IMPORTED_MODULE_0__["default"].fromRange( range );
const marker = new Marker( markerName, liveRange, managedUsingOperations, affectsData );
this._markers.set( markerName, marker );
this.fire( 'update:' + markerName, marker, null, range );
return marker;
}
/**
* Removes given {@link ~Marker marker} or a marker with given name from the `MarkerCollection`.
*
* @protected
* @fires module:engine/model/markercollection~MarkerCollection#event:update
* @param {String} markerOrName Marker or name of a marker to remove.
* @returns {Boolean} `true` if marker was found and removed, `false` otherwise.
*/
_remove( markerOrName ) {
const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
const oldMarker = this._markers.get( markerName );
if ( oldMarker ) {
this._markers.delete( markerName );
this.fire( 'update:' + markerName, oldMarker, oldMarker.getRange(), null );
this._destroyMarker( oldMarker );
return true;
}
return false;
}
/**
* Fires an {@link module:engine/model/markercollection~MarkerCollection#event:update} event for the given {@link ~Marker marker}
* but does not change the marker. Useful to force {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast
* conversion} for the marker.
*
* @protected
* @fires module:engine/model/markercollection~MarkerCollection#event:update
* @param {String} markerOrName Marker or name of a marker to refresh.
*/
_refresh( markerOrName ) {
const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
const marker = this._markers.get( markerName );
if ( !marker ) {
/**
* Marker with provided name does not exists.
*
* @error markercollection-refresh-marker-not-exists
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'markercollection-refresh-marker-not-exists', this );
}
const range = marker.getRange();
this.fire( 'update:' + markerName, marker, range, range, marker.managedUsingOperations, marker.affectsData );
}
/**
* Returns iterator that iterates over all markers, which ranges contain given {@link module:engine/model/position~Position position}.
*
* @param {module:engine/model/position~Position} position
* @returns {Iterable.}
*/
* getMarkersAtPosition( position ) {
for ( const marker of this ) {
if ( marker.getRange().containsPosition( position ) ) {
yield marker;
}
}
}
/**
* Returns iterator that iterates over all markers, which intersects with given {@link module:engine/model/range~Range range}.
*
* @param {module:engine/model/range~Range} range
* @returns {Iterable.}
*/
* getMarkersIntersectingRange( range ) {
for ( const marker of this ) {
if ( marker.getRange().getIntersection( range ) !== null ) {
yield marker;
}
}
}
/**
* Destroys marker collection and all markers inside it.
*/
destroy() {
for ( const marker of this._markers.values() ) {
this._destroyMarker( marker );
}
this._markers = null;
this.stopListening();
}
/**
* Iterates over all markers that starts with given `prefix`.
*
* const markerFooA = markersCollection.set( 'foo:a', rangeFooA );
* const markerFooB = markersCollection.set( 'foo:b', rangeFooB );
* const markerBarA = markersCollection.set( 'bar:a', rangeBarA );
* const markerFooBarA = markersCollection.set( 'foobar:a', rangeFooBarA );
* Array.from( markersCollection.getMarkersGroup( 'foo' ) ); // [ markerFooA, markerFooB ]
* Array.from( markersCollection.getMarkersGroup( 'a' ) ); // []
*
* @param prefix
* @returns {Iterable.}
*/
* getMarkersGroup( prefix ) {
for ( const marker of this._markers.values() ) {
if ( marker.name.startsWith( prefix + ':' ) ) {
yield marker;
}
}
}
/**
* Destroys the marker.
*
* @private
* @param {module:engine/model/markercollection~Marker} marker Marker to destroy.
*/
_destroyMarker( marker ) {
marker.stopListening();
marker._detachLiveRange();
}
/**
* Fired whenever marker is added, updated or removed from `MarkerCollection`.
*
* @event update
* @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
* means that marker is just removed.
*/
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_3__["default"])( MarkerCollection, _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__["default"] );
/**
* `Marker` is a continuous parts of model (like a range), is named and represent some kind of information about marked
* part of model document. In contrary to {@link module:engine/model/node~Node nodes}, which are building blocks of
* model document tree, markers are not stored directly in document tree but in
* {@link module:engine/model/model~Model#markers model markers' collection}. Still, they are document data, by giving
* additional meaning to the part of a model document between marker start and marker end.
*
* In this sense, markers are similar to adding and converting attributes on nodes. The difference is that attribute is
* connected with a given node (e.g. a character is bold no matter if it gets moved or content around it changes).
* Markers on the other hand are continuous ranges and are characterized by their start and end position. This means that
* any character in the marker is marked by the marker. For example, if a character is moved outside of marker it stops being
* "special" and the marker is shrunk. Similarly, when a character is moved into the marker from other place in document
* model, it starts being "special" and the marker is enlarged.
*
* Another upside of markers is that finding marked part of document is fast and easy. Using attributes to mark some nodes
* and then trying to find that part of document would require traversing whole document tree. Marker gives instant access
* to the range which it is marking at the moment.
*
* Markers are built from a name and a range.
*
* Range of the marker is updated automatically when document changes, using
* {@link module:engine/model/liverange~LiveRange live range} mechanism.
*
* Name is used to group and identify markers. Names have to be unique, but markers can be grouped by
* using common prefixes, separated with `:`, for example: `user:john` or `search:3`. That's useful in term of creating
* namespaces for custom elements (e.g. comments, highlights). You can use this prefixes in
* {@link module:engine/model/markercollection~MarkerCollection#event:update} listeners to listen on changes in a group of markers.
* For instance: `model.markers.on( 'update:user', callback );` will be called whenever any `user:*` markers changes.
*
* There are two types of markers.
*
* 1. Markers managed directly, without using operations. They are added directly by {@link module:engine/model/writer~Writer}
* to the {@link module:engine/model/markercollection~MarkerCollection} without any additional mechanism. They can be used
* as bookmarks or visual markers. They are great for showing results of the find, or select link when the focus is in the input.
*
* 1. Markers managed using operations. These markers are also stored in {@link module:engine/model/markercollection~MarkerCollection}
* but changes in these markers is managed the same way all other changes in the model structure - using operations.
* Therefore, they are handled in the undo stack and synchronized between clients if the collaboration plugin is enabled.
* This type of markers is useful for solutions like spell checking or comments.
*
* Both type of them should be added / updated by {@link module:engine/model/writer~Writer#addMarker}
* and removed by {@link module:engine/model/writer~Writer#removeMarker} methods.
*
* model.change( ( writer ) => {
* const marker = writer.addMarker( name, { range, usingOperation: true } );
*
* // ...
*
* writer.removeMarker( marker );
* } );
*
* See {@link module:engine/model/writer~Writer} to find more examples.
*
* Since markers need to track change in the document, for efficiency reasons, it is best to create and keep as little
* markers as possible and remove them as soon as they are not needed anymore.
*
* Markers can be downcasted and upcasted.
*
* Markers downcast happens on {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker} and
* {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:removeMarker} events.
* Use {@link module:engine/conversion/downcasthelpers downcast converters} or attach a custom converter to mentioned events.
* For {@link module:engine/controller/datacontroller~DataController data pipeline}, marker should be downcasted to an element.
* Then, it can be upcasted back to a marker. Again, use {@link module:engine/conversion/upcasthelpers upcast converters} or
* attach a custom converter to {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher#event:element}.
*
* `Marker` instances are created and destroyed only by {@link ~MarkerCollection MarkerCollection}.
*/
class Marker {
/**
* Creates a marker instance.
*
* @param {String} name Marker name.
* @param {module:engine/model/liverange~LiveRange} liveRange Range marked by the marker.
* @param {Boolean} managedUsingOperations Specifies whether the marker is managed using operations.
* @param {Boolean} affectsData Specifies whether the marker affects the data produced by the data pipeline
* (is persisted in the editor's data).
*/
constructor( name, liveRange, managedUsingOperations, affectsData ) {
/**
* Marker's name.
*
* @readonly
* @type {String}
*/
this.name = name;
/**
* Range marked by the marker.
*
* @protected
* @member {module:engine/model/liverange~LiveRange}
*/
this._liveRange = this._attachLiveRange( liveRange );
/**
* Flag indicates if the marker is managed using operations or not.
*
* @private
* @member {Boolean}
*/
this._managedUsingOperations = managedUsingOperations;
/**
* Specifies whether the marker affects the data produced by the data pipeline
* (is persisted in the editor's data).
*
* @private
* @member {Boolean}
*/
this._affectsData = affectsData;
}
/**
* A value indicating if the marker is managed using operations.
* See {@link ~Marker marker class description} to learn more about marker types.
* See {@link module:engine/model/writer~Writer#addMarker}.
*
* @returns {Boolean}
*/
get managedUsingOperations() {
if ( !this._liveRange ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'marker-destroyed', this );
}
return this._managedUsingOperations;
}
/**
* A value indicating if the marker changes the data.
*
* @returns {Boolean}
*/
get affectsData() {
if ( !this._liveRange ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'marker-destroyed', this );
}
return this._affectsData;
}
/**
* Returns current marker start position.
*
* @returns {module:engine/model/position~Position}
*/
getStart() {
if ( !this._liveRange ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'marker-destroyed', this );
}
return this._liveRange.start.clone();
}
/**
* Returns current marker end position.
*
* @returns {module:engine/model/position~Position}
*/
getEnd() {
if ( !this._liveRange ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'marker-destroyed', this );
}
return this._liveRange.end.clone();
}
/**
* Returns a range that represents the current state of the marker.
*
* Keep in mind that returned value is a {@link module:engine/model/range~Range Range}, not a
* {@link module:engine/model/liverange~LiveRange LiveRange}. This means that it is up-to-date and relevant only
* until next model document change. Do not store values returned by this method. Instead, store {@link ~Marker#name}
* and get `Marker` instance from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection} every
* time there is a need to read marker properties. This will guarantee that the marker has not been removed and
* that it's data is up-to-date.
*
* @returns {module:engine/model/range~Range}
*/
getRange() {
if ( !this._liveRange ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'marker-destroyed', this );
}
return this._liveRange.toRange();
}
/**
* Checks whether this object is of the given.
*
* marker.is( 'marker' ); // -> true
* marker.is( 'model:marker' ); // -> true
*
* marker.is( 'view:element' ); // -> false
* marker.is( 'documentSelection' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'marker' || type === 'model:marker';
}
/**
* Binds new live range to the marker and detach the old one if is attached.
*
* @protected
* @param {module:engine/model/liverange~LiveRange} liveRange Live range to attach
* @returns {module:engine/model/liverange~LiveRange} Attached live range.
*/
_attachLiveRange( liveRange ) {
if ( this._liveRange ) {
this._detachLiveRange();
}
// Delegating does not work with namespaces. Alternatively, we could delegate all events (using `*`).
liveRange.delegate( 'change:range' ).to( this );
liveRange.delegate( 'change:content' ).to( this );
this._liveRange = liveRange;
return liveRange;
}
/**
* Unbinds and destroys currently attached live range.
*
* @protected
*/
_detachLiveRange() {
this._liveRange.stopDelegating( 'change:range', this );
this._liveRange.stopDelegating( 'change:content', this );
this._liveRange.detach();
this._liveRange = null;
}
/**
* Fired whenever {@link ~Marker#_liveRange marker range} is changed due to changes on {@link module:engine/model/document~Document}.
* This is a delegated {@link module:engine/model/liverange~LiveRange#event:change:range LiveRange change:range event}.
*
* When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
* all event listeners listening to it should be removed. It is best to do it on
* {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
*
* @see module:engine/model/liverange~LiveRange#event:change:range
* @event change:range
* @param {module:engine/model/range~Range} oldRange
* @param {Object} data
*/
/**
* Fired whenever change on {@link module:engine/model/document~Document} is done inside {@link ~Marker#_liveRange marker range}.
* This is a delegated {@link module:engine/model/liverange~LiveRange#event:change:content LiveRange change:content event}.
*
* When marker is removed from {@link module:engine/model/markercollection~MarkerCollection MarkerCollection},
* all event listeners listening to it should be removed. It is best to do it on
* {@link module:engine/model/markercollection~MarkerCollection#event:update MarkerCollection update event}.
*
* @see module:engine/model/liverange~LiveRange#event:change:content
* @event change:content
* @param {module:engine/model/range~Range} oldRange
* @param {Object} data
*/
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_3__["default"])( Marker, _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_1__["default"] );
/**
* Cannot use a {@link module:engine/model/markercollection~MarkerCollection#destroy destroyed marker} instance.
*
* @error marker-destroyed
*/
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/model.js":
/*!********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/model.js ***!
\********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Model; });
/* harmony import */ var _batch__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./batch */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/batch.js");
/* harmony import */ var _writer__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./writer */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/writer.js");
/* harmony import */ var _schema__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./schema */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/schema.js");
/* harmony import */ var _document__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./document */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/document.js");
/* harmony import */ var _markercollection__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./markercollection */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/markercollection.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_observablemixin__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/observablemixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/observablemixin.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/* harmony import */ var _element__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./element */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _selection__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./selection */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/selection.js");
/* harmony import */ var _operation_operationfactory__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./operation/operationfactory */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operationfactory.js");
/* harmony import */ var _utils_insertcontent__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./utils/insertcontent */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/utils/insertcontent.js");
/* harmony import */ var _utils_deletecontent__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./utils/deletecontent */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/utils/deletecontent.js");
/* harmony import */ var _utils_modifyselection__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./utils/modifyselection */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/utils/modifyselection.js");
/* harmony import */ var _utils_getselectedcontent__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ./utils/getselectedcontent */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/utils/getselectedcontent.js");
/* harmony import */ var _utils_selection_post_fixer__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ./utils/selection-post-fixer */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/utils/selection-post-fixer.js");
/* harmony import */ var _utils_autoparagraphing__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ./utils/autoparagraphing */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/utils/autoparagraphing.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/model
*/
// @if CK_DEBUG_ENGINE // const { dumpTrees } = require( '../dev-utils/utils' );
// @if CK_DEBUG_ENGINE // const { OperationReplayer } = require( '../dev-utils/operationreplayer' ).default;
/**
* Editor's data model. Read about the model in the
* {@glink framework/guides/architecture/editing-engine engine architecture guide}.
*
* @mixes module:utils/observablemixin~ObservableMixin
*/
class Model {
constructor() {
/**
* Model's marker collection.
*
* @readonly
* @member {module:engine/model/markercollection~MarkerCollection}
*/
this.markers = new _markercollection__WEBPACK_IMPORTED_MODULE_4__["default"]();
/**
* Model's document.
*
* @readonly
* @member {module:engine/model/document~Document}
*/
this.document = new _document__WEBPACK_IMPORTED_MODULE_3__["default"]( this );
/**
* Model's schema.
*
* @readonly
* @member {module:engine/model/schema~Schema}
*/
this.schema = new _schema__WEBPACK_IMPORTED_MODULE_2__["default"]();
/**
* All callbacks added by {@link module:engine/model/model~Model#change} or
* {@link module:engine/model/model~Model#enqueueChange} methods waiting to be executed.
*
* @private
* @type {Array.}
*/
this._pendingChanges = [];
/**
* The last created and currently used writer instance.
*
* @private
* @member {module:engine/model/writer~Writer}
*/
this._currentWriter = null;
[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
.forEach( methodName => this.decorate( methodName ) );
// Adding operation validation with `highest` priority, so it is called before any other feature would like
// to do anything with the operation. If the operation has incorrect parameters it should throw on the earliest occasion.
this.on( 'applyOperation', ( evt, args ) => {
const operation = args[ 0 ];
operation._validate();
}, { priority: 'highest' } );
// Register some default abstract entities.
this.schema.register( '$root', {
isLimit: true
} );
this.schema.register( '$block', {
allowIn: '$root',
isBlock: true
} );
this.schema.register( '$text', {
allowIn: '$block',
isInline: true,
isContent: true
} );
this.schema.register( '$clipboardHolder', {
allowContentOf: '$root',
isLimit: true
} );
this.schema.extend( '$text', { allowIn: '$clipboardHolder' } );
// An element needed by the `upcastElementToMarker` converter.
// This element temporarily represents a marker boundary during the conversion process and is removed
// at the end of the conversion. `UpcastDispatcher` or at least `Conversion` class looks like a
// better place for this registration but both know nothing about `Schema`.
this.schema.register( '$marker' );
this.schema.addChildCheck( ( context, childDefinition ) => {
if ( childDefinition.name === '$marker' ) {
return true;
}
} );
Object(_utils_selection_post_fixer__WEBPACK_IMPORTED_MODULE_16__["injectSelectionPostFixer"])( this );
// Post-fixer which takes care of adding empty paragraph elements to the empty roots.
this.document.registerPostFixer( _utils_autoparagraphing__WEBPACK_IMPORTED_MODULE_17__["autoParagraphEmptyRoots"] );
// @if CK_DEBUG_ENGINE // this.on( 'applyOperation', () => {
// @if CK_DEBUG_ENGINE // dumpTrees( this.document, this.document.version );
// @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
}
/**
* The `change()` method is the primary way of changing the model. You should use it to modify all document nodes
* (including detached nodes – i.e. nodes not added to the {@link module:engine/model/model~Model#document model document}),
* the {@link module:engine/model/document~Document#selection document's selection}, and
* {@link module:engine/model/model~Model#markers model markers}.
*
* model.change( writer => {
* writer.insertText( 'foo', paragraph, 'end' );
* } );
*
* All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they are combined
* into a single undo step.
*
* model.change( writer => {
* writer.insertText( 'foo', paragraph, 'end' ); // foo.
*
* model.change( writer => {
* writer.insertText( 'bar', paragraph, 'end' ); // foobar.
* } );
*
* writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
* } );
*
* The callback of the `change()` block is executed synchronously.
*
* You can also return a value from the change block.
*
* const img = model.change( writer => {
* return writer.createElement( 'img' );
* } );
*
* @see #enqueueChange
* @param {Function} callback Callback function which may modify the model.
* @returns {*} Value returned by the callback.
*/
change( callback ) {
try {
if ( this._pendingChanges.length === 0 ) {
// If this is the outermost block, create a new batch and start `_runPendingChanges` execution flow.
this._pendingChanges.push( { batch: new _batch__WEBPACK_IMPORTED_MODULE_0__["default"](), callback } );
return this._runPendingChanges()[ 0 ];
} else {
// If this is not the outermost block, just execute the callback.
return callback( this._currentWriter );
}
} catch ( err ) {
// @if CK_DEBUG // throw err;
/* istanbul ignore next */
_ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_18__["default"].rethrowUnexpectedError( err, this );
}
}
/**
* The `enqueueChange()` method performs similar task as the {@link #change `change()` method}, with two major differences.
*
* First, the callback of `enqueueChange()` is executed when all other enqueued changes are done. It might be executed
* immediately if it is not nested in any other change block, but if it is nested in another (enqueue)change block,
* it will be delayed and executed after the outermost block.
*
* model.change( writer => {
* console.log( 1 );
*
* model.enqueueChange( writer => {
* console.log( 2 );
* } );
*
* console.log( 3 );
* } ); // Will log: 1, 3, 2.
*
* In addition to that, the changes enqueued with `enqueueChange()` will be converted separately from the changes
* done in the outer `change()` block.
*
* Second, it lets you define the {@link module:engine/model/batch~Batch} into which you want to add your changes.
* By default, a new batch is created. In the sample above, `change` and `enqueueChange` blocks use a different
* batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
*
* When using the `enqueueChange()` block you can also add some changes to the batch you used before.
*
* model.enqueueChange( batch, writer => {
* writer.insertText( 'foo', paragraph, 'end' );
* } );
*
* In order to make a nested `enqueueChange()` create a single undo step together with the changes done in the outer `change()`
* block, you can obtain the batch instance from the {@link module:engine/model/writer~Writer#batch writer} of the outer block.
*
* @param {module:engine/model/batch~Batch|'transparent'|'default'} batchOrType Batch or batch type should be used in the callback.
* If not defined, a new batch will be created.
* @param {Function} callback Callback function which may modify the model.
*/
enqueueChange( batchOrType, callback ) {
try {
if ( typeof batchOrType === 'string' ) {
batchOrType = new _batch__WEBPACK_IMPORTED_MODULE_0__["default"]( batchOrType );
} else if ( typeof batchOrType == 'function' ) {
callback = batchOrType;
batchOrType = new _batch__WEBPACK_IMPORTED_MODULE_0__["default"]();
}
this._pendingChanges.push( { batch: batchOrType, callback } );
if ( this._pendingChanges.length == 1 ) {
this._runPendingChanges();
}
} catch ( err ) {
// @if CK_DEBUG // throw err;
/* istanbul ignore next */
_ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_18__["default"].rethrowUnexpectedError( err, this );
}
}
/**
* {@link module:utils/observablemixin~ObservableMixin#decorate Decorated} function for applying
* {@link module:engine/model/operation/operation~Operation operations} to the model.
*
* This is a low-level way of changing the model. It is exposed for very specific use cases (like the undo feature).
* Normally, to modify the model, you will want to use {@link module:engine/model/writer~Writer `Writer`}.
* See also {@glink framework/guides/architecture/editing-engine#changing-the-model Changing the model} section
* of the {@glink framework/guides/architecture/editing-engine Editing architecture} guide.
*
* @param {module:engine/model/operation/operation~Operation} operation The operation to apply.
*/
applyOperation( operation ) {
// @if CK_DEBUG_ENGINE // console.log( 'Applying ' + operation );
// @if CK_DEBUG_ENGINE // if ( !this._operationLogs ) {
// @if CK_DEBUG_ENGINE // this._operationLogs = [];
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // this._operationLogs.push( JSON.stringify( operation ) );
// @if CK_DEBUG_ENGINE //if ( !this._appliedOperations ) {
// @if CK_DEBUG_ENGINE // this._appliedOperations = [];
// @if CK_DEBUG_ENGINE //}
// @if CK_DEBUG_ENGINE //this._appliedOperations.push( operation );
operation._execute();
}
// @if CK_DEBUG_ENGINE // getAppliedOperation() {
// @if CK_DEBUG_ENGINE // if ( !this._appliedOperations ) {
// @if CK_DEBUG_ENGINE // return '';
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // return this._appliedOperations.map( JSON.stringify ).join( '-------' );
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // createReplayer( stringifiedOperations ) {
// @if CK_DEBUG_ENGINE // return new OperationReplayer( this, '-------', stringifiedOperations );
// @if CK_DEBUG_ENGINE // }
/**
* Inserts content at the position in the editor specified by the selection, as one would expect the paste
* functionality to work.
*
* This is a high-level method. It takes the {@link #schema schema} into consideration when inserting
* the content, clears the given selection's content before inserting nodes and moves the selection
* to its target position at the end of the process.
* It can split elements, merge them, wrap bare text nodes with paragraphs, etc. — just like the
* pasting feature should do.
*
* For lower-level methods see {@link module:engine/model/writer~Writer `Writer`}.
*
* This method, unlike {@link module:engine/model/writer~Writer `Writer`}'s methods, does not have to be used
* inside a {@link #change `change()` block}.
*
* # Conversion and schema
*
* Inserting elements and text nodes into the model is not enough to make CKEditor 5 render that content
* to the user. CKEditor 5 implements a model-view-controller architecture and what `model.insertContent()` does
* is only adding nodes to the model. Additionally, you need to define
* {@glink framework/guides/architecture/editing-engine#conversion converters} between the model and view
* and define those nodes in the {@glink framework/guides/architecture/editing-engine#schema schema}.
*
* So, while this method may seem similar to CKEditor 4 `editor.insertHtml()` (in fact, both methods
* are used for paste-like content insertion), the CKEditor 5 method cannot be use to insert arbitrary HTML
* unless converters are defined for all elements and attributes in that HTML.
*
* # Examples
*
* Using `insertContent()` with a manually created model structure:
*
* // Let's create a document fragment containing such content as:
* //
* // foo
* //
* // bar
* //
* const docFrag = editor.model.change( writer => {
* const p1 = writer.createElement( 'paragraph' );
* const p2 = writer.createElement( 'paragraph' );
* const blockQuote = writer.createElement( 'blockQuote' );
* const docFrag = writer.createDocumentFragment();
*
* writer.append( p1, docFrag );
* writer.append( blockQuote, docFrag );
* writer.append( p2, blockQuote );
* writer.insertText( 'foo', p1 );
* writer.insertText( 'bar', p2 );
*
* return docFrag;
* } );
*
* // insertContent() does not have to be used in a change() block. It can, though,
* // so this code could be moved to the callback defined above.
* editor.model.insertContent( docFrag );
*
* Using `insertContent()` with an HTML string converted to a model document fragment (similar to the pasting mechanism):
*
* // You can create your own HtmlDataProcessor instance or use editor.data.processor
* // if you have not overridden the default one (which is the HtmlDataProcessor instance).
* const htmlDP = new HtmlDataProcessor( viewDocument );
*
* // Convert an HTML string to a view document fragment:
* const viewFragment = htmlDP.toView( htmlString );
*
* // Convert the view document fragment to a model document fragment
* // in the context of $root. This conversion takes the schema into
* // account so if, for example, the view document fragment contained a bare text node,
* // this text node cannot be a child of $root, so it will be automatically
* // wrapped with a . You can define the context yourself (in the second parameter),
* // and e.g. convert the content like it would happen in a .
* // Note: The clipboard feature uses a custom context called $clipboardHolder
* // which has a loosened schema.
* const modelFragment = editor.data.toModel( viewFragment );
*
* editor.model.insertContent( modelFragment );
*
* By default this method will use the document selection but it can also be used with a position, range or selection instance.
*
* // Insert text at the current document selection position.
* editor.model.change( writer => {
* editor.model.insertContent( writer.createText( 'x' ) );
* } );
*
* // Insert text at a given position - the document selection will not be modified.
* editor.model.change( writer => {
* editor.model.insertContent( writer.createText( 'x' ), doc.getRoot(), 2 );
*
* // Which is a shorthand for:
* editor.model.insertContent( writer.createText( 'x' ), writer.createPositionAt( doc.getRoot(), 2 ) );
* } );
*
* If you want the document selection to be moved to the inserted content, use the
* {@link module:engine/model/writer~Writer#setSelection `setSelection()`} method of the writer after inserting
* the content:
*
* editor.model.change( writer => {
* const paragraph = writer.createElement( 'paragraph' );
*
* // Insert an empty paragraph at the beginning of the root.
* editor.model.insertContent( paragraph, writer.createPositionAt( editor.model.document.getRoot(), 0 ) );
*
* // Move the document selection to the inserted paragraph.
* writer.setSelection( paragraph, 'in' );
* } );
*
* If an instance of the {@link module:engine/model/selection~Selection model selection} is passed as `selectable`,
* the new content will be inserted at the passed selection (instead of document selection):
*
* editor.model.change( writer => {
* // Create a selection in a paragraph that will be used as a place of insertion.
* const selection = writer.createSelection( paragraph, 'in' );
*
* // Insert the new text at the created selection.
* editor.model.insertContent( writer.createText( 'x' ), selection );
*
* // insertContent() modifies the passed selection instance so it can be used to set the document selection.
* // Note: This is not necessary when you passed the document selection to insertContent().
* writer.setSelection( selection );
* } );
*
* @fires insertContent
* @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
* @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
* The selection into which the content should be inserted. If not provided the current model document selection will be used.
* @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`.
* This param defines a position in relation to that item.
* @returns {module:engine/model/range~Range} Range which contains all the performed changes. This is a range that, if removed,
* would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
* at the insertion position.
*/
insertContent( content, selectable, placeOrOffset ) {
return Object(_utils_insertcontent__WEBPACK_IMPORTED_MODULE_12__["default"])( this, content, selectable, placeOrOffset );
}
/**
* Deletes content of the selection and merge siblings. The resulting selection is always collapsed.
*
* **Note:** For the sake of predictability, the resulting selection should always be collapsed.
* In cases where a feature wants to modify deleting behavior so selection isn't collapsed
* (e.g. a table feature may want to keep row selection after pressing Backspace ),
* then that behavior should be implemented in the view's listener. At the same time, the table feature
* will need to modify this method's behavior too, e.g. to "delete contents and then collapse
* the selection inside the last selected cell" or "delete the row and collapse selection somewhere near".
* That needs to be done in order to ensure that other features which use `deleteContent()` will work well with tables.
*
* @fires deleteContent
* @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
* Selection of which the content should be deleted.
* @param {Object} [options]
* @param {Boolean} [options.leaveUnmerged=false] Whether to merge elements after removing the content of the selection.
*
* For example `x[x y]y ` will become:
*
* * `x^y ` with the option disabled (`leaveUnmerged == false`)
* * `x^ y ` with enabled (`leaveUnmerged == true`).
*
* Note: {@link module:engine/model/schema~Schema#isObject object} and {@link module:engine/model/schema~Schema#isLimit 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 `[x y] ` will become:
*
* * `^ ` with the option disabled (`doNotResetEntireContent == false`)
* * `^ ` with enabled (`doNotResetEntireContent == true`)
*
* @param {Boolean} [options.doNotAutoparagraph=false] Whether to create a paragraph if after content deletion selection is moved
* to a place where text cannot be inserted.
*
* For example `x [ ]` will become:
*
* * `x [] ` with the option disabled (`doNotAutoparagraph == false`)
* * `x[] ` with the option enabled (`doNotAutoparagraph == true`).
*
* **Note:** if there is no valid position for the selection, the paragraph will always be created:
*
* `[ ]` -> `[] `.
*
* @param {'forward'|'backward'} [options.direction='backward'] The direction in which the content is being consumed.
* Deleting backward corresponds to using the Backspace key, while deleting content forward corresponds to
* the Shift +Backspace keystroke.
*/
deleteContent( selection, options ) {
Object(_utils_deletecontent__WEBPACK_IMPORTED_MODULE_13__["default"])( this, selection, options );
}
/**
* Modifies the selection. Currently, the supported modifications are:
*
* * Extending. The selection focus is moved in the specified `options.direction` with a step specified in `options.unit`.
* Possible values for `unit` are:
* * `'character'` (default) - moves selection by one user-perceived character. In most cases this means moving by one
* character in `String` sense. However, unicode also defines "combing marks". These are special symbols, that combines
* with a symbol before it ("base character") to create one user-perceived character. For example, `q̣̇` is a normal
* letter `q` with two "combining marks": upper dot (`Ux0307`) and lower dot (`Ux0323`). For most actions, i.e. extending
* selection by one position, it is correct to include both "base character" and all of it's "combining marks". That is
* why `'character'` value is most natural and common method of modifying selection.
* * `'codePoint'` - moves selection by one unicode code point. In contrary to, `'character'` unit, this will insert
* selection between "base character" and "combining mark", because "combining marks" have their own unicode code points.
* However, for technical reasons, unicode code points with values above `UxFFFF` are represented in native `String` by
* two characters, called "surrogate pairs". Halves of "surrogate pairs" have a meaning only when placed next to each other.
* For example `𨭎` is represented in `String` by `\uD862\uDF4E`. Both `\uD862` and `\uDF4E` do not have any meaning
* outside the pair (are rendered as ? when alone). Position between them would be incorrect. In this case, selection
* extension will include whole "surrogate pair".
* * `'word'` - moves selection by a whole word.
*
* **Note:** if you extend a forward selection in a backward direction you will in fact shrink it.
*
* @fires modifySelection
* @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
* The selection to modify.
* @param {Object} [options]
* @param {'forward'|'backward'} [options.direction='forward'] The direction in which the selection should be modified.
* @param {'character'|'codePoint'|'word'} [options.unit='character'] The unit by which selection should be modified.
*/
modifySelection( selection, options ) {
Object(_utils_modifyselection__WEBPACK_IMPORTED_MODULE_14__["default"])( this, selection, options );
}
/**
* Gets a clone of the selected content.
*
* For example, for the following selection:
*
* ```html
* x
*
* y
* fir[st
*
* se]cond
* z
* ```
*
* It will return a document fragment with such a content:
*
* ```html
*
* st
*
* se
* ```
*
* @fires getSelectedContent
* @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
* The selection of which content will be returned.
* @returns {module:engine/model/documentfragment~DocumentFragment}
*/
getSelectedContent( selection ) {
return Object(_utils_getselectedcontent__WEBPACK_IMPORTED_MODULE_15__["default"])( this, selection );
}
/**
* Checks whether the given {@link module:engine/model/range~Range range} or
* {@link module:engine/model/element~Element element} has any meaningful content.
*
* Meaningful content is:
*
* * any text node (`options.ignoreWhitespaces` allows controlling whether this text node must also contain
* any non-whitespace characters),
* * or any {@link module:engine/model/schema~Schema#isContent content element},
* * or any {@link module:engine/model/markercollection~Marker marker} which
* {@link module:engine/model/markercollection~Marker#_affectsData affects data}.
*
* This means that a range containing an empty ` ` is not considered to have a meaningful content.
* However, a range containing an ` ` (which would normally be marked in the schema as an object element)
* is considered non-empty.
*
* @param {module:engine/model/range~Range|module:engine/model/element~Element} rangeOrElement Range or element to check.
* @param {Object} [options]
* @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 = {} ) {
const range = rangeOrElement instanceof _element__WEBPACK_IMPORTED_MODULE_7__["default"] ? _range__WEBPACK_IMPORTED_MODULE_8__["default"]._createIn( rangeOrElement ) : rangeOrElement;
if ( range.isCollapsed ) {
return false;
}
const { ignoreWhitespaces = false, ignoreMarkers = false } = options;
// Check if there are any markers which affects data in this given range.
if ( !ignoreMarkers ) {
for ( const intersectingMarker of this.markers.getMarkersIntersectingRange( range ) ) {
if ( intersectingMarker.affectsData ) {
return true;
}
}
}
for ( const item of range.getItems() ) {
if ( this.schema.isContent( item ) ) {
if ( item.is( '$textProxy' ) ) {
if ( !ignoreWhitespaces ) {
return true;
} else if ( item.data.search( /\S/ ) !== -1 ) {
return true;
}
} else {
return true;
}
}
}
return false;
}
/**
* Creates a position from the given root and path in that root.
*
* Note: This method is also available as
* {@link module:engine/model/writer~Writer#createPositionFromPath `Writer#createPositionFromPath()`}.
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
* @param {Array.} path Position path. See {@link module:engine/model/position~Position#path}.
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
* See {@link module:engine/model/position~PositionStickiness}.
* @returns {module:engine/model/position~Position}
*/
createPositionFromPath( root, path, stickiness ) {
return new _position__WEBPACK_IMPORTED_MODULE_9__["default"]( root, path, stickiness );
}
/**
* Creates position at the given location. The location can be specified as:
*
* * a {@link module:engine/model/position~Position position},
* * a parent element and offset in that element,
* * a parent element and `'end'` (the position will be set at the end of that element),
* * a {@link module:engine/model/item~Item model item} and `'before'` or `'after'`
* (the position will be set before or after the given model item).
*
* This method is a shortcut to other factory methods such as:
*
* * {@link module:engine/model/model~Model#createPositionBefore `createPositionBefore()`},
* * {@link module:engine/model/model~Model#createPositionAfter `createPositionAfter()`}.
*
* Note: This method is also available as
* {@link module:engine/model/writer~Writer#createPositionAt `Writer#createPositionAt()`},
*
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/model/item~Item model item}.
*/
createPositionAt( itemOrPosition, offset ) {
return _position__WEBPACK_IMPORTED_MODULE_9__["default"]._createAt( itemOrPosition, offset );
}
/**
* Creates a new position after the given {@link module:engine/model/item~Item model item}.
*
* Note: This method is also available as
* {@link module:engine/model/writer~Writer#createPositionAfter `Writer#createPositionAfter()`}.
*
* @param {module:engine/model/item~Item} item Item after which the position should be placed.
* @returns {module:engine/model/position~Position}
*/
createPositionAfter( item ) {
return _position__WEBPACK_IMPORTED_MODULE_9__["default"]._createAfter( item );
}
/**
* Creates a new position before the given {@link module:engine/model/item~Item model item}.
*
* Note: This method is also available as
* {@link module:engine/model/writer~Writer#createPositionBefore `Writer#createPositionBefore()`}.
*
* @param {module:engine/model/item~Item} item Item before which the position should be placed.
* @returns {module:engine/model/position~Position}
*/
createPositionBefore( item ) {
return _position__WEBPACK_IMPORTED_MODULE_9__["default"]._createBefore( item );
}
/**
* Creates a range spanning from the `start` position to the `end` position.
*
* Note: This method is also available as
* {@link module:engine/model/writer~Writer#createRange `Writer#createRange()`}:
*
* model.change( writer => {
* const range = writer.createRange( start, end );
* } );
*
* @param {module:engine/model/position~Position} start Start position.
* @param {module:engine/model/position~Position} [end] End position. If not set, the range will be collapsed
* to the `start` position.
* @returns {module:engine/model/range~Range}
*/
createRange( start, end ) {
return new _range__WEBPACK_IMPORTED_MODULE_8__["default"]( start, end );
}
/**
* Creates a range inside the given element which starts before the first child of
* that element and ends after the last child of that element.
*
* Note: This method is also available as
* {@link module:engine/model/writer~Writer#createRangeIn `Writer#createRangeIn()`}:
*
* model.change( writer => {
* const range = writer.createRangeIn( paragraph );
* } );
*
* @param {module:engine/model/element~Element} element Element which is a parent for the range.
* @returns {module:engine/model/range~Range}
*/
createRangeIn( element ) {
return _range__WEBPACK_IMPORTED_MODULE_8__["default"]._createIn( element );
}
/**
* Creates a range that starts before the given {@link module:engine/model/item~Item model item} and ends after it.
*
* Note: This method is also available on `writer` instance as
* {@link module:engine/model/writer~Writer#createRangeOn `Writer.createRangeOn()`}:
*
* model.change( writer => {
* const range = writer.createRangeOn( paragraph );
* } );
*
* @param {module:engine/model/item~Item} item
* @returns {module:engine/model/range~Range}
*/
createRangeOn( item ) {
return _range__WEBPACK_IMPORTED_MODULE_8__["default"]._createOn( item );
}
/**
* Creates a new selection instance based on the given {@link module:engine/model/selection~Selectable selectable}
* or creates an empty selection if no arguments were passed.
*
* Note: This method is also available as
* {@link module:engine/model/writer~Writer#createSelection `Writer#createSelection()`}.
*
* // Creates empty selection without ranges.
* const selection = writer.createSelection();
*
* // Creates selection at the given range.
* const range = writer.createRange( start, end );
* const selection = writer.createSelection( range );
*
* // Creates selection at the given ranges
* const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
* const selection = writer.createSelection( ranges );
*
* // Creates selection from the other selection.
* // Note: It doesn't copies selection attributes.
* const otherSelection = writer.createSelection();
* const selection = writer.createSelection( otherSelection );
*
* // Creates selection from the given document selection.
* // Note: It doesn't copies selection attributes.
* const documentSelection = model.document.selection;
* const selection = writer.createSelection( documentSelection );
*
* // Creates selection at the given position.
* const position = writer.createPositionFromPath( root, path );
* const selection = writer.createSelection( position );
*
* // Creates selection at the given offset in the given element.
* const paragraph = writer.createElement( 'paragraph' );
* const selection = writer.createSelection( paragraph, offset );
*
* // Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
* // first child of that element and ends after the last child of that element.
* const selection = writer.createSelection( paragraph, 'in' );
*
* // Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
* // just after the item.
* const selection = writer.createSelection( paragraph, 'on' );
*
* // Additional options (`'backward'`) can be specified as the last argument.
*
* // Creates backward selection.
* const selection = writer.createSelection( range, { backward: true } );
*
* @param {module:engine/model/selection~Selectable} selectable
* @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
* @param {Object} [options]
* @param {Boolean} [options.backward] Sets this selection instance to be backward.
* @returns {module:engine/model/selection~Selection}
*/
createSelection( selectable, placeOrOffset, options ) {
return new _selection__WEBPACK_IMPORTED_MODULE_10__["default"]( selectable, placeOrOffset, options );
}
/**
* Creates a {@link module:engine/model/batch~Batch} instance.
*
* **Note:** In most cases creating a batch instance is not necessary as they are created when using:
*
* * {@link #change `change()`},
* * {@link #enqueueChange `enqueueChange()`}.
*
* @param {'transparent'|'default'} [type='default'] The type of the batch.
* @returns {module:engine/model/batch~Batch}
*/
createBatch( type ) {
return new _batch__WEBPACK_IMPORTED_MODULE_0__["default"]( type );
}
/**
* Creates an operation instance from a JSON object (parsed JSON string).
*
* This is an alias for {@link module:engine/model/operation/operationfactory~OperationFactory.fromJSON `OperationFactory.fromJSON()`}.
*
* @param {Object} json Deserialized JSON object.
* @returns {module:engine/model/operation/operation~Operation}
*/
createOperationFromJSON( json ) {
return _operation_operationfactory__WEBPACK_IMPORTED_MODULE_11__["default"].fromJSON( json, this.document );
}
/**
* Removes all events listeners set by model instance and destroys {@link module:engine/model/document~Document}.
*/
destroy() {
this.document.destroy();
this.stopListening();
}
/**
* Common part of {@link module:engine/model/model~Model#change} and {@link module:engine/model/model~Model#enqueueChange}
* which calls callbacks and returns array of values returned by these callbacks.
*
* @private
* @returns {Array.<*>} Array of values returned by callbacks.
*/
_runPendingChanges() {
const ret = [];
this.fire( '_beforeChanges' );
while ( this._pendingChanges.length ) {
// Create a new writer using batch instance created for this chain of changes.
const currentBatch = this._pendingChanges[ 0 ].batch;
this._currentWriter = new _writer__WEBPACK_IMPORTED_MODULE_1__["default"]( this, currentBatch );
// Execute changes callback and gather the returned value.
const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
ret.push( callbackReturnValue );
this.document._handleChangeBlock( this._currentWriter );
this._pendingChanges.shift();
this._currentWriter = null;
}
this.fire( '_afterChanges' );
return ret;
}
/**
* Fired when entering the outermost {@link module:engine/model/model~Model#enqueueChange} or
* {@link module:engine/model/model~Model#change} block.
*
* @protected
* @event _beforeChanges
*/
/**
* Fired when leaving the outermost {@link module:engine/model/model~Model#enqueueChange} or
* {@link module:engine/model/model~Model#change} block.
*
* @protected
* @event _afterChanges
*/
/**
* Fired every time any {@link module:engine/model/operation/operation~Operation operation} is applied on the model
* using {@link #applyOperation}.
*
* Note that this event is suitable only for very specific use-cases. Use it if you need to listen to every single operation
* applied on the document. However, in most cases {@link module:engine/model/document~Document#event:change} should
* be used.
*
* A few callbacks are already added to this event by engine internal classes:
*
* * with `highest` priority operation is validated,
* * with `normal` priority operation is executed,
* * with `low` priority the {@link module:engine/model/document~Document} updates its version,
* * with `low` priority {@link module:engine/model/liveposition~LivePosition} and {@link module:engine/model/liverange~LiveRange}
* update themselves.
*
* @event applyOperation
* @param {Array} args Arguments of the `applyOperation` which is an array with a single element - applied
* {@link module:engine/model/operation/operation~Operation operation}.
*/
/**
* Event fired when {@link #insertContent} method is called.
*
* The {@link #insertContent default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* **Note** The `selectable` parameter for the {@link #insertContent} is optional. When `undefined` value is passed the method uses
* `model.document.selection`.
*
* @event insertContent
* @param {Array} args The arguments passed to the original method.
*/
/**
* Event fired when {@link #deleteContent} method is called.
*
* The {@link #deleteContent default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* @event deleteContent
* @param {Array} args The arguments passed to the original method.
*/
/**
* Event fired when {@link #modifySelection} method is called.
*
* The {@link #modifySelection default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* @event modifySelection
* @param {Array} args The arguments passed to the original method.
*/
/**
* Event fired when {@link #getSelectedContent} method is called.
*
* The {@link #getSelectedContent default action of that method} is implemented as a
* listener to this event so it can be fully customized by the features.
*
* @event getSelectedContent
* @param {Array} args The arguments passed to the original method.
*/
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_6__["default"])( Model, _ckeditor_ckeditor5_utils_src_observablemixin__WEBPACK_IMPORTED_MODULE_5__["default"] );
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js":
/*!*******************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js ***!
\*******************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Node; });
/* harmony import */ var _ckeditor_ckeditor5_utils_src_tomap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/tomap */ "./node_modules/@ckeditor/ckeditor5-utils/src/tomap.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/comparearrays */ "./node_modules/@ckeditor/ckeditor5-utils/src/comparearrays.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_version__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/version */ "./node_modules/@ckeditor/ckeditor5-utils/src/version.js");
/**
* @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 engine/model/node
*/
// To check if component is loaded more than once.
/**
* Model node. Most basic structure of model tree.
*
* This is an abstract class that is a base for other classes representing different nodes in model.
*
* **Note:** If a node is detached from the model tree, you can manipulate it using it's API.
* However, it is **very important** that nodes already attached to model tree should be only changed through
* {@link module:engine/model/writer~Writer Writer API}.
*
* Changes done by `Node` methods, like {@link module:engine/model/element~Element#_insertChild _insertChild} or
* {@link module:engine/model/node~Node#_setAttribute _setAttribute}
* do not generate {@link module:engine/model/operation/operation~Operation operations}
* which are essential for correct editor work if you modify nodes in {@link module:engine/model/document~Document document} root.
*
* The flow of working on `Node` (and classes that inherits from it) is as such:
* 1. You can create a `Node` instance, modify it using it's API.
* 2. Add `Node` to the model using `Batch` API.
* 3. Change `Node` that was already added to the model using `Batch` API.
*
* Similarly, you cannot use `Batch` API on a node that has not been added to the model tree, with the exception
* of {@link module:engine/model/writer~Writer#insert inserting} that node to the model tree.
*
* Be aware that using {@link module:engine/model/writer~Writer#remove remove from Batch API} does not allow to use `Node` API because
* the information about `Node` is still kept in model document.
*
* In case of {@link module:engine/model/element~Element element node}, adding and removing children also counts as changing a node and
* follows same rules.
*/
class Node {
/**
* Creates a model node.
*
* This is an abstract class, so this constructor should not be used directly.
*
* @abstract
* @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
*/
constructor( attrs ) {
/**
* Parent of this node. It could be {@link module:engine/model/element~Element}
* or {@link module:engine/model/documentfragment~DocumentFragment}.
* Equals to `null` if the node has no parent.
*
* @readonly
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
this.parent = null;
/**
* Attributes set on this node.
*
* @private
* @member {Map} module:engine/model/node~Node#_attrs
*/
this._attrs = Object(_ckeditor_ckeditor5_utils_src_tomap__WEBPACK_IMPORTED_MODULE_0__["default"])( attrs );
}
/**
* Index of this node in it's parent or `null` if the node has no parent.
*
* Accessing this property throws an error if this node's parent element does not contain it.
* This means that model tree got broken.
*
* @readonly
* @type {Number|null}
*/
get index() {
let pos;
if ( !this.parent ) {
return null;
}
if ( ( pos = this.parent.getChildIndex( this ) ) === null ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"]( 'model-node-not-found-in-parent', this );
}
return pos;
}
/**
* Offset at which this node starts in it's parent. It is equal to the sum of {@link #offsetSize offsetSize}
* of all it's previous siblings. Equals to `null` if node has no parent.
*
* Accessing this property throws an error if this node's parent element does not contain it.
* This means that model tree got broken.
*
* @readonly
* @type {Number|null}
*/
get startOffset() {
let pos;
if ( !this.parent ) {
return null;
}
if ( ( pos = this.parent.getChildStartOffset( this ) ) === null ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"]( 'model-node-not-found-in-parent', this );
}
return pos;
}
/**
* Offset size of this node. Represents how much "offset space" is occupied by the node in it's parent.
* It is important for {@link module:engine/model/position~Position position}. When node has `offsetSize` greater than `1`, position
* can be placed between that node start and end. `offsetSize` greater than `1` is for nodes that represents more
* than one entity, i.e. {@link module:engine/model/text~Text text node}.
*
* @readonly
* @type {Number}
*/
get offsetSize() {
return 1;
}
/**
* Offset at which this node ends in it's parent. It is equal to the sum of this node's
* {@link module:engine/model/node~Node#startOffset start offset} and {@link #offsetSize offset size}.
* Equals to `null` if the node has no parent.
*
* @readonly
* @type {Number|null}
*/
get endOffset() {
if ( !this.parent ) {
return null;
}
return this.startOffset + this.offsetSize;
}
/**
* Node's next sibling or `null` if the node is a last child of it's parent or if the node has no parent.
*
* @readonly
* @type {module:engine/model/node~Node|null}
*/
get nextSibling() {
const index = this.index;
return ( index !== null && this.parent.getChild( index + 1 ) ) || null;
}
/**
* Node's previous sibling or `null` if the node is a first child of it's parent or if the node has no parent.
*
* @readonly
* @type {module:engine/model/node~Node|null}
*/
get previousSibling() {
const index = this.index;
return ( index !== null && this.parent.getChild( index - 1 ) ) || null;
}
/**
* The top-most ancestor of the node. If node has no parent it is the root itself. If the node is a part
* of {@link module:engine/model/documentfragment~DocumentFragment}, it's `root` is equal to that `DocumentFragment`.
*
* @readonly
* @type {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
let root = this; // eslint-disable-line consistent-this
while ( root.parent ) {
root = root.parent;
}
return root;
}
/**
* Returns true if the node is in a tree rooted in the document (is a descendant of one of its roots).
*
* @returns {Boolean}
*/
isAttached() {
return this.root.is( 'rootElement' );
}
/**
* Gets path to the node. The path is an array containing starting offsets of consecutive ancestors of this node,
* beginning from {@link module:engine/model/node~Node#root root}, down to this node's starting offset. The path can be used to
* create {@link module:engine/model/position~Position Position} instance.
*
* const abc = new Text( 'abc' );
* const foo = new Text( 'foo' );
* const h1 = new Element( 'h1', null, new Text( 'header' ) );
* const p = new Element( 'p', null, [ abc, foo ] );
* const div = new Element( 'div', null, [ h1, p ] );
* foo.getPath(); // Returns [ 1, 3 ]. `foo` is in `p` which is in `div`. `p` starts at offset 1, while `foo` at 3.
* h1.getPath(); // Returns [ 0 ].
* div.getPath(); // Returns [].
*
* @returns {Array.} The path.
*/
getPath() {
const path = [];
let node = this; // eslint-disable-line consistent-this
while ( node.parent ) {
path.unshift( node.startOffset );
node = node.parent;
}
return path;
}
/**
* Returns ancestors array of this node.
*
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` this node will be also included in parent's array.
* @param {Boolean} [options.parentFirst=false] When set to `true`, array will be sorted from node's parent to root element,
* otherwise root element will be the first item in the array.
* @returns {Array} Array with ancestors.
*/
getAncestors( options = { includeSelf: false, parentFirst: false } ) {
const ancestors = [];
let parent = options.includeSelf ? this : this.parent;
while ( parent ) {
ancestors[ options.parentFirst ? 'push' : 'unshift' ]( parent );
parent = parent.parent;
}
return ancestors;
}
/**
* Returns a {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of both nodes.
*
* @param {module:engine/model/node~Node} node The second node.
* @param {Object} options Options object.
* @param {Boolean} [options.includeSelf=false] When set to `true` both nodes will be considered "ancestors" too.
* Which means that if e.g. node A is inside B, then their common ancestor will be B.
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( node, options = {} ) {
const ancestorsA = this.getAncestors( options );
const ancestorsB = node.getAncestors( options );
let i = 0;
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}
return i === 0 ? null : ancestorsA[ i - 1 ];
}
/**
* Returns whether this node is before given node. `false` is returned if nodes are in different trees (for example,
* in different {@link module:engine/model/documentfragment~DocumentFragment}s).
*
* @param {module:engine/model/node~Node} node Node to compare with.
* @returns {Boolean}
*/
isBefore( node ) {
// Given node is not before this node if they are same.
if ( this == node ) {
return false;
}
// Return `false` if it is impossible to compare nodes.
if ( this.root !== node.root ) {
return false;
}
const thisPath = this.getPath();
const nodePath = node.getPath();
const result = Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_2__["default"])( thisPath, nodePath );
switch ( result ) {
case 'prefix':
return true;
case 'extension':
return false;
default:
return thisPath[ result ] < nodePath[ result ];
}
}
/**
* Returns whether this node is after given node. `false` is returned if nodes are in different trees (for example,
* in different {@link module:engine/model/documentfragment~DocumentFragment}s).
*
* @param {module:engine/model/node~Node} node Node to compare with.
* @returns {Boolean}
*/
isAfter( node ) {
// Given node is not before this node if they are same.
if ( this == node ) {
return false;
}
// Return `false` if it is impossible to compare nodes.
if ( this.root !== node.root ) {
return false;
}
// In other cases, just check if the `node` is before, and return the opposite.
return !this.isBefore( node );
}
/**
* Checks if the node has an attribute with given key.
*
* @param {String} key Key of attribute to check.
* @returns {Boolean} `true` if attribute with given key is set on node, `false` otherwise.
*/
hasAttribute( key ) {
return this._attrs.has( key );
}
/**
* Gets an attribute value for given key or `undefined` if that attribute is not set on node.
*
* @param {String} key Key of attribute to look for.
* @returns {*} Attribute value or `undefined`.
*/
getAttribute( key ) {
return this._attrs.get( key );
}
/**
* Returns iterator that iterates over this node's attributes.
*
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
*
* @returns {Iterable.<*>}
*/
getAttributes() {
return this._attrs.entries();
}
/**
* Returns iterator that iterates over this node's attribute keys.
*
* @returns {Iterable.}
*/
getAttributeKeys() {
return this._attrs.keys();
}
/**
* Converts `Node` to plain object and returns it.
*
* @returns {Object} `Node` converted to plain object.
*/
toJSON() {
const json = {};
// Serializes attributes to the object.
// attributes = { a: 'foo', b: 1, c: true }.
if ( this._attrs.size ) {
json.attributes = Array.from( this._attrs ).reduce( ( result, attr ) => {
result[ attr[ 0 ] ] = attr[ 1 ];
return result;
}, {} );
}
return json;
}
/**
* Checks whether this object is of the given type.
*
* This method is useful when processing model objects that are of unknown type. For example, a function
* may return a {@link module:engine/model/documentfragment~DocumentFragment} or a {@link module:engine/model/node~Node}
* that can be either a text node or an element. This method can be used to check what kind of object is returned.
*
* someObject.is( 'element' ); // -> true if this is an element
* someObject.is( 'node' ); // -> true if this is a node (a text node or an element)
* someObject.is( 'documentFragment' ); // -> true if this is a document fragment
*
* Since this method is also available on a range of view objects, you can prefix the type of the object with
* `model:` or `view:` to check, for example, if this is the model's or view's element:
*
* modelElement.is( 'model:element' ); // -> true
* modelElement.is( 'view:element' ); // -> false
*
* By using this method it is also possible to check a name of an element:
*
* imageElement.is( 'element', 'image' ); // -> true
* imageElement.is( 'element', 'image' ); // -> same as above
* imageElement.is( 'model:element', 'image' ); // -> same as above, but more precise
*
* The list of model objects which implement the `is()` method:
*
* * {@link module:engine/model/node~Node#is `Node#is()`}
* * {@link module:engine/model/text~Text#is `Text#is()`}
* * {@link module:engine/model/element~Element#is `Element#is()`}
* * {@link module:engine/model/rootelement~RootElement#is `RootElement#is()`}
* * {@link module:engine/model/position~Position#is `Position#is()`}
* * {@link module:engine/model/liveposition~LivePosition#is `LivePosition#is()`}
* * {@link module:engine/model/range~Range#is `Range#is()`}
* * {@link module:engine/model/liverange~LiveRange#is `LiveRange#is()`}
* * {@link module:engine/model/documentfragment~DocumentFragment#is `DocumentFragment#is()`}
* * {@link module:engine/model/selection~Selection#is `Selection#is()`}
* * {@link module:engine/model/documentselection~DocumentSelection#is `DocumentSelection#is()`}
* * {@link module:engine/model/markercollection~Marker#is `Marker#is()`}
* * {@link module:engine/model/textproxy~TextProxy#is `TextProxy#is()`}
*
* @method #is
* @param {String} type Type to check.
* @returns {Boolean}
*/
is( type ) {
return type === 'node' || type === 'model:node';
}
/**
* Creates a copy of this node, that is a node with exactly same attributes, and returns it.
*
* @protected
* @returns {module:engine/model/node~Node} Node with same attributes as this node.
*/
_clone() {
return new Node( this._attrs );
}
/**
* Removes this node from it's parent.
*
* @see module:engine/model/writer~Writer#remove
* @protected
*/
_remove() {
this.parent._removeChildren( this.index );
}
/**
* Sets attribute on the node. If attribute with the same key already is set, it's value is overwritten.
*
* @see module:engine/model/writer~Writer#setAttribute
* @protected
* @param {String} key Key of attribute to set.
* @param {*} value Attribute value.
*/
_setAttribute( key, value ) {
this._attrs.set( key, value );
}
/**
* Removes all attributes from the node and sets given attributes.
*
* @see module:engine/model/writer~Writer#setAttributes
* @protected
* @param {Object} [attrs] Attributes to set. See {@link module:utils/tomap~toMap} for a list of accepted values.
*/
_setAttributesTo( attrs ) {
this._attrs = Object(_ckeditor_ckeditor5_utils_src_tomap__WEBPACK_IMPORTED_MODULE_0__["default"])( attrs );
}
/**
* Removes an attribute with given key from the node.
*
* @see module:engine/model/writer~Writer#removeAttribute
* @protected
* @param {String} key Key of attribute to remove.
* @returns {Boolean} `true` if the attribute was set on the element, `false` otherwise.
*/
_removeAttribute( key ) {
return this._attrs.delete( key );
}
/**
* Removes all attributes from the node.
*
* @see module:engine/model/writer~Writer#clearAttributes
* @protected
*/
_clearAttributes() {
this._attrs.clear();
}
}
/**
* The node's parent does not contain this node.
*
* @error model-node-not-found-in-parent
*/
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js":
/*!***********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js ***!
\***********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return NodeList; });
/* harmony import */ var _node__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/nodelist
*/
/**
* Provides an interface to operate on a list of {@link module:engine/model/node~Node nodes}. `NodeList` is used internally
* in classes like {@link module:engine/model/element~Element Element}
* or {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment}.
*/
class NodeList {
/**
* Creates an empty node list.
*
* @protected
* @param {Iterable.} nodes Nodes contained in this node list.
*/
constructor( nodes ) {
/**
* Nodes contained in this node list.
*
* @private
* @member {Array.}
*/
this._nodes = [];
if ( nodes ) {
this._insertNodes( 0, nodes );
}
}
/**
* Iterable interface.
*
* Iterates over all nodes contained inside this node list.
*
* @returns {Iterable.}
*/
[ Symbol.iterator ]() {
return this._nodes[ Symbol.iterator ]();
}
/**
* Number of nodes contained inside this node list.
*
* @readonly
* @type {Number}
*/
get length() {
return this._nodes.length;
}
/**
* Sum of {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes contained inside this node list.
*
* @readonly
* @type {Number}
*/
get maxOffset() {
return this._nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}
/**
* Gets the node at the given index. Returns `null` if incorrect index was passed.
*
* @param {Number} index Index of node.
* @returns {module:engine/model/node~Node|null} Node at given index.
*/
getNode( index ) {
return this._nodes[ index ] || null;
}
/**
* Returns an index of the given node. Returns `null` if given node is not inside this node list.
*
* @param {module:engine/model/node~Node} node Child node to look for.
* @returns {Number|null} Child node's index.
*/
getNodeIndex( node ) {
const index = this._nodes.indexOf( node );
return index == -1 ? null : index;
}
/**
* Returns the starting offset of given node. Starting offset is equal to the sum of
* {@link module:engine/model/node~Node#offsetSize offset sizes} of all nodes that are before this node in this node list.
*
* @param {module:engine/model/node~Node} node Node to look for.
* @returns {Number|null} Node's starting offset.
*/
getNodeStartOffset( node ) {
const index = this.getNodeIndex( node );
return index === null ? null : this._nodes.slice( 0, index ).reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}
/**
* Converts index to offset in node list.
*
* Returns starting offset of a node that is at given index. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
* `model-nodelist-index-out-of-bounds` if given index is less than `0` or more than {@link #length}.
*
* @param {Number} index Node's index.
* @returns {Number} Node's starting offset.
*/
indexToOffset( index ) {
if ( index == this._nodes.length ) {
return this.maxOffset;
}
const node = this._nodes[ index ];
if ( !node ) {
/**
* Given index cannot be found in the node list.
*
* @error model-nodelist-index-out-of-bounds
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"]( 'model-nodelist-index-out-of-bounds', this );
}
return this.getNodeStartOffset( node );
}
/**
* Converts offset in node list to index.
*
* Returns index of a node that occupies given offset. Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError}
* `model-nodelist-offset-out-of-bounds` if given offset is less than `0` or more than {@link #maxOffset}.
*
* @param {Number} offset Offset to look for.
* @returns {Number} Index of a node that occupies given offset.
*/
offsetToIndex( offset ) {
let totalOffset = 0;
for ( const node of this._nodes ) {
if ( offset >= totalOffset && offset < totalOffset + node.offsetSize ) {
return this.getNodeIndex( node );
}
totalOffset += node.offsetSize;
}
if ( totalOffset != offset ) {
/**
* Given offset cannot be found in the node list.
*
* @error model-nodelist-offset-out-of-bounds
* @param {Number} offset
* @param {module:engine/model/nodelist~NodeList} nodeList Stringified node list.
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"]( 'model-nodelist-offset-out-of-bounds',
this,
{
offset,
nodeList: this
}
);
}
return this.length;
}
/**
* Inserts given nodes at given index.
*
* @protected
* @param {Number} index Index at which nodes should be inserted.
* @param {Iterable.} nodes Nodes to be inserted.
*/
_insertNodes( index, nodes ) {
// Validation.
for ( const node of nodes ) {
if ( !( node instanceof _node__WEBPACK_IMPORTED_MODULE_0__["default"] ) ) {
/**
* Trying to insert an object which is not a Node instance.
*
* @error model-nodelist-insertnodes-not-node
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"]( 'model-nodelist-insertnodes-not-node', this );
}
}
this._nodes.splice( index, 0, ...nodes );
}
/**
* Removes one or more nodes starting at the given index.
*
* @protected
* @param {Number} indexStart Index of the first node to remove.
* @param {Number} [howMany=1] Number of nodes to remove.
* @returns {Array.} Array containing removed nodes.
*/
_removeNodes( indexStart, howMany = 1 ) {
return this._nodes.splice( indexStart, howMany );
}
/**
* Converts `NodeList` instance to an array containing nodes that were inserted in the node list. Nodes
* are also converted to their plain object representation.
*
* @returns {Array.} `NodeList` instance converted to `Array`.
*/
toJSON() {
return this._nodes.map( node => node.toJSON() );
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/attributeoperation.js":
/*!*******************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/attributeoperation.js ***!
\*******************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return AttributeOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./utils */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js");
/* harmony import */ var lodash_es__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! lodash-es */ "./node_modules/lodash-es/lodash.js");
/**
* @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 engine/model/operation/attributeoperation
*/
/**
* Operation to change nodes' attribute.
*
* Using this class you can add, remove or change value of the attribute.
*
* @extends module:engine/model/operation/operation~Operation
*/
class AttributeOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates an operation that changes, removes or adds attributes.
*
* If only `newValue` is set, attribute will be added on a node. Note that all nodes in operation's range must not
* have an attribute with the same key as the added attribute.
*
* If only `oldValue` is set, then attribute with given key will be removed. Note that all nodes in operation's range
* must have an attribute with that key added.
*
* If both `newValue` and `oldValue` are set, then the operation will change the attribute value. Note that all nodes in
* operation's ranges must already have an attribute with given key and `oldValue` as value
*
* @param {module:engine/model/range~Range} range Range on which the operation should be applied. Must be a flat range.
* @param {String} key Key of an attribute to change or remove.
* @param {*} oldValue Old value of the attribute with given key or `null`, if attribute was not set before.
* @param {*} newValue New value of the attribute with given key or `null`, if operation should remove attribute.
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( range, key, oldValue, newValue, baseVersion ) {
super( baseVersion );
/**
* Range on which operation should be applied.
*
* @readonly
* @member {module:engine/model/range~Range}
*/
this.range = range.clone();
/**
* Key of an attribute to change or remove.
*
* @readonly
* @member {String}
*/
this.key = key;
/**
* Old value of the attribute with given key or `null`, if attribute was not set before.
*
* @readonly
* @member {*}
*/
this.oldValue = oldValue === undefined ? null : oldValue;
/**
* New value of the attribute with given key or `null`, if operation should remove attribute.
*
* @readonly
* @member {*}
*/
this.newValue = newValue === undefined ? null : newValue;
}
/**
* @inheritDoc
*/
get type() {
if ( this.oldValue === null ) {
return 'addAttribute';
} else if ( this.newValue === null ) {
return 'removeAttribute';
} else {
return 'changeAttribute';
}
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation} Clone of this operation.
*/
clone() {
return new AttributeOperation( this.range, this.key, this.oldValue, this.newValue, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
*/
getReversed() {
return new AttributeOperation( this.range, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
json.range = this.range.toJSON();
return json;
}
/**
* @inheritDoc
*/
_validate() {
if ( !this.range.isFlat ) {
/**
* The range to change is not flat.
*
* @error attribute-operation-range-not-flat
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'attribute-operation-range-not-flat', this );
}
for ( const item of this.range.getItems( { shallow: true } ) ) {
if ( this.oldValue !== null && !Object(lodash_es__WEBPACK_IMPORTED_MODULE_4__["isEqual"])( item.getAttribute( this.key ), this.oldValue ) ) {
/**
* Changed node has different attribute value than operation's old attribute value.
*
* @error attribute-operation-wrong-old-value
* @param {module:engine/model/item~Item} item
* @param {String} key
* @param {*} value
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'attribute-operation-wrong-old-value',
this,
{ item, key: this.key, value: this.oldValue }
);
}
if ( this.oldValue === null && this.newValue !== null && item.hasAttribute( this.key ) ) {
/**
* The attribute with given key already exists for the given node.
*
* @error attribute-operation-attribute-exists
* @param {module:engine/model/node~Node} node
* @param {String} key
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'attribute-operation-attribute-exists',
this,
{ node: item, key: this.key }
);
}
}
}
/**
* @inheritDoc
*/
_execute() {
// If value to set is same as old value, don't do anything.
if ( !Object(lodash_es__WEBPACK_IMPORTED_MODULE_4__["isEqual"])( this.oldValue, this.newValue ) ) {
// Execution.
Object(_utils__WEBPACK_IMPORTED_MODULE_3__["_setAttribute"])( this.range, this.key, this.newValue );
}
}
/**
* @inheritDoc
*/
static get className() {
return 'AttributeOperation';
}
/**
* Creates `AttributeOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
*/
static fromJSON( json, document ) {
return new AttributeOperation( _range__WEBPACK_IMPORTED_MODULE_1__["default"].fromJSON( json.range, document ), json.key, json.oldValue, json.newValue, json.baseVersion );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `AttributeOperation( ${ this.baseVersion } ): ` +
// @if CK_DEBUG_ENGINE // `"${ this.key }": ${ JSON.stringify( this.oldValue ) }` +
// @if CK_DEBUG_ENGINE // ` -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/detachoperation.js":
/*!****************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/detachoperation.js ***!
\****************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return DetachOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/operation/detachoperation
*/
// @if CK_DEBUG_ENGINE // const ModelRange = require( '../range' ).default;
/**
* Operation to permanently remove node from detached root.
* Note this operation is only a local operation and won't be send to the other clients.
*
* @extends module:engine/model/operation/operation~Operation
*/
class DetachOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates an insert operation.
*
* @param {module:engine/model/position~Position} sourcePosition
* Position before the first {@link module:engine/model/item~Item model item} to move.
* @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
* `sourcePosition` with offset shifted by `howMany`.
*/
constructor( sourcePosition, howMany ) {
super( null );
/**
* Position before the first {@link module:engine/model/item~Item model item} to detach.
*
* @member {module:engine/model/position~Position} #sourcePosition
*/
this.sourcePosition = sourcePosition.clone();
/**
* Offset size of moved range.
*
* @member {Number} #howMany
*/
this.howMany = howMany;
}
/**
* @inheritDoc
*/
get type() {
return 'detach';
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
json.sourcePosition = this.sourcePosition.toJSON();
return json;
}
/**
* @inheritDoc
*/
_validate() {
if ( this.sourcePosition.root.document ) {
/**
* Cannot detach document node.
*
* @error detach-operation-on-document-node
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__["default"]( 'detach-operation-on-document-node', this );
}
}
/**
* @inheritDoc
*/
_execute() {
Object(_utils__WEBPACK_IMPORTED_MODULE_2__["_remove"])( _range__WEBPACK_IMPORTED_MODULE_1__["default"]._createFromPositionAndShift( this.sourcePosition, this.howMany ) );
}
/**
* @inheritDoc
*/
static get className() {
return 'DetachOperation';
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
// @if CK_DEBUG_ENGINE // const nodes = Array.from( range.getItems() );
// @if CK_DEBUG_ENGINE // const nodeString = nodes.length > 1 ? `[ ${ nodes.length } ]` : nodes[ 0 ];
// @if CK_DEBUG_ENGINE // return `DetachOperation( ${ this.baseVersion } ): ${ nodeString } -> ${ range }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/insertoperation.js":
/*!****************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/insertoperation.js ***!
\****************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return InsertOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _nodelist__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../nodelist */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js");
/* harmony import */ var _moveoperation__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./moveoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/moveoperation.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./utils */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js");
/* harmony import */ var _text__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../text */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js");
/* harmony import */ var _element__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../element */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/operation/insertoperation
*/
/**
* Operation to insert one or more nodes at given position in the model.
*
* @extends module:engine/model/operation/operation~Operation
*/
class InsertOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates an insert operation.
*
* @param {module:engine/model/position~Position} position Position of insertion.
* @param {module:engine/model/node~NodeSet} nodes The list of nodes to be inserted.
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( position, nodes, baseVersion ) {
super( baseVersion );
/**
* Position of insertion.
*
* @readonly
* @member {module:engine/model/position~Position} module:engine/model/operation/insertoperation~InsertOperation#position
*/
this.position = position.clone();
this.position.stickiness = 'toNone';
/**
* List of nodes to insert.
*
* @readonly
* @member {module:engine/model/nodelist~NodeList} module:engine/model/operation/insertoperation~InsertOperation#nodeList
*/
this.nodes = new _nodelist__WEBPACK_IMPORTED_MODULE_2__["default"]( Object(_utils__WEBPACK_IMPORTED_MODULE_4__["_normalizeNodes"])( nodes ) );
/**
* Flag deciding how the operation should be transformed. If set to `true`, nodes might get additional attributes
* during operational transformation. This happens when the operation insertion position is inside of a range
* where attributes have changed.
*
* @member {Boolean} module:engine/model/operation/insertoperation~InsertOperation#shouldReceiveAttributes
*/
this.shouldReceiveAttributes = false;
}
/**
* @inheritDoc
*/
get type() {
return 'insert';
}
/**
* Total offset size of inserted nodes.
*
* @returns {Number}
*/
get howMany() {
return this.nodes.maxOffset;
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/insertoperation~InsertOperation} Clone of this operation.
*/
clone() {
const nodes = new _nodelist__WEBPACK_IMPORTED_MODULE_2__["default"]( [ ...this.nodes ].map( node => node._clone( true ) ) );
const insert = new InsertOperation( this.position, nodes, this.baseVersion );
insert.shouldReceiveAttributes = this.shouldReceiveAttributes;
return insert;
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/moveoperation~MoveOperation}
*/
getReversed() {
const graveyard = this.position.root.document.graveyard;
const gyPosition = new _position__WEBPACK_IMPORTED_MODULE_1__["default"]( graveyard, [ 0 ] );
return new _moveoperation__WEBPACK_IMPORTED_MODULE_3__["default"]( this.position, this.nodes.maxOffset, gyPosition, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_validate() {
const targetElement = this.position.parent;
if ( !targetElement || targetElement.maxOffset < this.position.offset ) {
/**
* Insertion position is invalid.
*
* @error insert-operation-position-invalid
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_7__["default"](
'insert-operation-position-invalid',
this
);
}
}
/**
* @inheritDoc
*/
_execute() {
// What happens here is that we want original nodes be passed to writer because we want original nodes
// to be inserted to the model. But in InsertOperation, we want to keep those nodes as they were added
// to the operation, not modified. For example, text nodes can get merged or cropped while Elements can
// get children. It is important that InsertOperation has the copy of original nodes in intact state.
const originalNodes = this.nodes;
this.nodes = new _nodelist__WEBPACK_IMPORTED_MODULE_2__["default"]( [ ...originalNodes ].map( node => node._clone( true ) ) );
Object(_utils__WEBPACK_IMPORTED_MODULE_4__["_insert"])( this.position, originalNodes );
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
json.position = this.position.toJSON();
json.nodes = this.nodes.toJSON();
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'InsertOperation';
}
/**
* Creates `InsertOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/insertoperation~InsertOperation}
*/
static fromJSON( json, document ) {
const children = [];
for ( const child of json.nodes ) {
if ( child.name ) {
// If child has name property, it is an Element.
children.push( _element__WEBPACK_IMPORTED_MODULE_6__["default"].fromJSON( child ) );
} else {
// Otherwise, it is a Text node.
children.push( _text__WEBPACK_IMPORTED_MODULE_5__["default"].fromJSON( child ) );
}
}
const insert = new InsertOperation( _position__WEBPACK_IMPORTED_MODULE_1__["default"].fromJSON( json.position, document ), children, json.baseVersion );
insert.shouldReceiveAttributes = json.shouldReceiveAttributes;
return insert;
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // const nodeString = this.nodes.length > 1 ? `[ ${ this.nodes.length } ]` : this.nodes.getNode( 0 );
// @if CK_DEBUG_ENGINE // return `InsertOperation( ${ this.baseVersion } ): ${ nodeString } -> ${ this.position }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/markeroperation.js":
/*!****************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/markeroperation.js ***!
\****************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return MarkerOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/**
* @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 engine/model/operation/markeroperation
*/
/**
* @extends module:engine/model/operation/operation~Operation
*/
class MarkerOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* @param {String} name Marker name.
* @param {module:engine/model/range~Range} oldRange Marker range before the change.
* @param {module:engine/model/range~Range} newRange Marker range after the change.
* @param {module:engine/model/markercollection~MarkerCollection} markers Marker collection on which change should be executed.
* @param {Boolean} affectsData Specifies whether the marker operation affects the data produced by the data pipeline
* (is persisted in the editor's data).
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( name, oldRange, newRange, markers, affectsData, baseVersion ) {
super( baseVersion );
/**
* Marker name.
*
* @readonly
* @member {String}
*/
this.name = name;
/**
* Marker range before the change.
*
* @readonly
* @member {module:engine/model/range~Range}
*/
this.oldRange = oldRange ? oldRange.clone() : null;
/**
* Marker range after the change.
*
* @readonly
* @member {module:engine/model/range~Range}
*/
this.newRange = newRange ? newRange.clone() : null;
/**
* Specifies whether the marker operation affects the data produced by the data pipeline
* (is persisted in the editor's data).
*
* @readonly
* @member {Boolean}
*/
this.affectsData = affectsData;
/**
* Marker collection on which change should be executed.
*
* @private
* @member {module:engine/model/markercollection~MarkerCollection}
*/
this._markers = markers;
}
/**
* @inheritDoc
*/
get type() {
return 'marker';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/markeroperation~MarkerOperation} Clone of this operation.
*/
clone() {
return new MarkerOperation( this.name, this.oldRange, this.newRange, this._markers, this.affectsData, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/markeroperation~MarkerOperation}
*/
getReversed() {
return new MarkerOperation( this.name, this.newRange, this.oldRange, this._markers, this.affectsData, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_execute() {
const type = this.newRange ? '_set' : '_remove';
this._markers[ type ]( this.name, this.newRange, true, this.affectsData );
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
if ( this.oldRange ) {
json.oldRange = this.oldRange.toJSON();
}
if ( this.newRange ) {
json.newRange = this.newRange.toJSON();
}
delete json._markers;
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'MarkerOperation';
}
/**
* Creates `MarkerOperation` object from deserialized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/markeroperation~MarkerOperation}
*/
static fromJSON( json, document ) {
return new MarkerOperation(
json.name,
json.oldRange ? _range__WEBPACK_IMPORTED_MODULE_1__["default"].fromJSON( json.oldRange, document ) : null,
json.newRange ? _range__WEBPACK_IMPORTED_MODULE_1__["default"].fromJSON( json.newRange, document ) : null,
document.model.markers,
json.affectsData,
json.baseVersion
);
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `MarkerOperation( ${ this.baseVersion } ): ` +
// @if CK_DEBUG_ENGINE // `"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/mergeoperation.js":
/*!***************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/mergeoperation.js ***!
\***************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return MergeOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _splitoperation__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./splitoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/splitoperation.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./utils */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/operation/mergeoperation
*/
/**
* Operation to merge two {@link module:engine/model/element~Element 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}.
*
* @extends module:engine/model/operation/operation~Operation
*/
class MergeOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates a merge operation.
*
* @param {module:engine/model/position~Position} sourcePosition Position inside the merged element. All nodes from that
* element after that position will be moved to {@link ~#targetPosition}.
* @param {Number} howMany Summary offset size of nodes which will be moved from the merged element to the new parent.
* @param {module:engine/model/position~Position} targetPosition Position which the nodes from the merged elements will be moved to.
* @param {module:engine/model/position~Position} graveyardPosition Position in graveyard to which the merged element will be moved.
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#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 );
/**
* Position inside the merged element. All nodes from that element after that position will be moved to {@link ~#targetPosition}.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#sourcePosition
*/
this.sourcePosition = sourcePosition.clone();
// This is, and should always remain, the first position in its parent.
this.sourcePosition.stickiness = 'toPrevious';
/**
* Summary offset size of nodes which will be moved from the merged element to the new parent.
*
* @member {Number} module:engine/model/operation/mergeoperation~MergeOperation#howMany
*/
this.howMany = howMany;
/**
* Position which the nodes from the merged elements will be moved to.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#targetPosition
*/
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';
/**
* Position in graveyard to which the merged element will be moved.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/mergeoperation~MergeOperation#graveyardPosition
*/
this.graveyardPosition = graveyardPosition.clone();
}
/**
* @inheritDoc
*/
get type() {
return 'merge';
}
/**
* Position before the merged element (which will be deleted).
*
* @readonly
* @type {module:engine/model/position~Position}
*/
get deletionPosition() {
return new _position__WEBPACK_IMPORTED_MODULE_2__["default"]( 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.
*
* @readonly
* @type {module:engine/model/range~Range}
*/
get movedRange() {
const end = this.sourcePosition.getShiftedBy( Number.POSITIVE_INFINITY );
return new _range__WEBPACK_IMPORTED_MODULE_3__["default"]( this.sourcePosition, end );
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/mergeoperation~MergeOperation} Clone of this operation.
*/
clone() {
return new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.graveyardPosition, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/splitoperation~SplitOperation}
*/
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 _position__WEBPACK_IMPORTED_MODULE_2__["default"]( this.sourcePosition.root, path )._getTransformedByMergeOperation( this );
const split = new _splitoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
split.insertionPosition = insertionPosition;
return split;
}
/**
* @inheritDoc
*/
_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 _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( '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 _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( '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 _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( 'merge-operation-how-many-invalid', this );
}
}
/**
* @inheritDoc
*/
_execute() {
const mergedElement = this.sourcePosition.parent;
const sourceRange = _range__WEBPACK_IMPORTED_MODULE_3__["default"]._createIn( mergedElement );
Object(_utils__WEBPACK_IMPORTED_MODULE_4__["_move"])( sourceRange, this.targetPosition );
Object(_utils__WEBPACK_IMPORTED_MODULE_4__["_move"])( _range__WEBPACK_IMPORTED_MODULE_3__["default"]._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 deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/mergeoperation~MergeOperation}
*/
static fromJSON( json, document ) {
const sourcePosition = _position__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( json.sourcePosition, document );
const targetPosition = _position__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( json.targetPosition, document );
const graveyardPosition = _position__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( json.graveyardPosition, document );
return new this( sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `MergeOperation( ${ this.baseVersion } ): ` +
// @if CK_DEBUG_ENGINE // `${ this.sourcePosition } -> ${ this.targetPosition }` +
// @if CK_DEBUG_ENGINE // ` ( ${ this.howMany } ), ${ this.graveyardPosition }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/moveoperation.js":
/*!**************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/moveoperation.js ***!
\**************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return MoveOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/comparearrays */ "./node_modules/@ckeditor/ckeditor5-utils/src/comparearrays.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./utils */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js");
/**
* @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 engine/model/operation/moveoperation
*/
// @if CK_DEBUG_ENGINE // const ModelRange = require( '../range' ).default;
/**
* Operation to move a range of {@link module:engine/model/item~Item model items}
* to given {@link module:engine/model/position~Position target position}.
*
* @extends module:engine/model/operation/operation~Operation
*/
class MoveOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates a move operation.
*
* @param {module:engine/model/position~Position} sourcePosition
* Position before the first {@link module:engine/model/item~Item model item} to move.
* @param {Number} howMany Offset size of moved range. Moved range will start from `sourcePosition` and end at
* `sourcePosition` with offset shifted by `howMany`.
* @param {module:engine/model/position~Position} targetPosition Position at which moved nodes will be inserted.
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( sourcePosition, howMany, targetPosition, baseVersion ) {
super( baseVersion );
/**
* Position before the first {@link module:engine/model/item~Item model item} to move.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#sourcePosition
*/
this.sourcePosition = sourcePosition.clone();
// `'toNext'` because `sourcePosition` is a bit like a start of the moved range.
this.sourcePosition.stickiness = 'toNext';
/**
* Offset size of moved range.
*
* @member {Number} module:engine/model/operation/moveoperation~MoveOperation#howMany
*/
this.howMany = howMany;
/**
* Position at which moved nodes will be inserted.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/moveoperation~MoveOperation#targetPosition
*/
this.targetPosition = targetPosition.clone();
this.targetPosition.stickiness = 'toNone';
}
/**
* @inheritDoc
*/
get type() {
if ( this.targetPosition.root.rootName == '$graveyard' ) {
return 'remove';
} else if ( this.sourcePosition.root.rootName == '$graveyard' ) {
return 'reinsert';
}
return 'move';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/moveoperation~MoveOperation} Clone of this operation.
*/
clone() {
return new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.baseVersion );
}
/**
* Returns the start position of the moved range after it got moved. This may be different than
* {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition} in some cases, i.e. when a range is moved
* inside the same parent but {@link module:engine/model/operation/moveoperation~MoveOperation#targetPosition targetPosition}
* is after {@link module:engine/model/operation/moveoperation~MoveOperation#sourcePosition sourcePosition}.
*
* vv vv
* abcdefg ===> adefbcg
* ^ ^
* targetPos movedRangeStart
* offset 6 offset 4
*
* @returns {module:engine/model/position~Position}
*/
getMovedRangeStart() {
return this.targetPosition._getTransformedByDeletion( this.sourcePosition, this.howMany );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/moveoperation~MoveOperation}
*/
getReversed() {
const newTargetPosition = this.sourcePosition._getTransformedByInsertion( this.targetPosition, this.howMany );
return new this.constructor( this.getMovedRangeStart(), this.howMany, newTargetPosition, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_validate() {
const sourceElement = this.sourcePosition.parent;
const targetElement = this.targetPosition.parent;
const sourceOffset = this.sourcePosition.offset;
const targetOffset = this.targetPosition.offset;
// Validate whether move operation has correct parameters.
// Validation is pretty complex but move operation is one of the core ways to manipulate the document state.
// We expect that many errors might be connected with one of scenarios described below.
if ( sourceOffset + this.howMany > sourceElement.maxOffset ) {
/**
* The nodes which should be moved do not exist.
*
* @error move-operation-nodes-do-not-exist
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__["default"](
'move-operation-nodes-do-not-exist', this
);
} else if ( sourceElement === targetElement && sourceOffset < targetOffset && targetOffset < sourceOffset + this.howMany ) {
/**
* Trying to move a range of nodes into the middle of that range.
*
* @error move-operation-range-into-itself
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__["default"](
'move-operation-range-into-itself', this
);
} else if ( this.sourcePosition.root == this.targetPosition.root ) {
if ( Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_4__["default"])( this.sourcePosition.getParentPath(), this.targetPosition.getParentPath() ) == 'prefix' ) {
const i = this.sourcePosition.path.length - 1;
if ( this.targetPosition.path[ i ] >= sourceOffset && this.targetPosition.path[ i ] < sourceOffset + this.howMany ) {
/**
* Trying to move a range of nodes into one of nodes from that range.
*
* @error move-operation-node-into-itself
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_3__["default"](
'move-operation-node-into-itself', this
);
}
}
}
}
/**
* @inheritDoc
*/
_execute() {
Object(_utils__WEBPACK_IMPORTED_MODULE_5__["_move"])( _range__WEBPACK_IMPORTED_MODULE_2__["default"]._createFromPositionAndShift( this.sourcePosition, this.howMany ), this.targetPosition );
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
json.sourcePosition = this.sourcePosition.toJSON();
json.targetPosition = this.targetPosition.toJSON();
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'MoveOperation';
}
/**
* Creates `MoveOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/moveoperation~MoveOperation}
*/
static fromJSON( json, document ) {
const sourcePosition = _position__WEBPACK_IMPORTED_MODULE_1__["default"].fromJSON( json.sourcePosition, document );
const targetPosition = _position__WEBPACK_IMPORTED_MODULE_1__["default"].fromJSON( json.targetPosition, document );
return new this( sourcePosition, json.howMany, targetPosition, json.baseVersion );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // const range = ModelRange._createFromPositionAndShift( this.sourcePosition, this.howMany );
// @if CK_DEBUG_ENGINE // return `MoveOperation( ${ this.baseVersion } ): ${ range } -> ${ this.targetPosition }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/nooperation.js":
/*!************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/nooperation.js ***!
\************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return NoOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/**
* @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 engine/model/operation/nooperation
*/
/**
* Operation which is doing nothing ("empty operation", "do-nothing operation", "noop"). This is an operation,
* which when executed does not change the tree model. It still has some parameters defined for transformation purposes.
*
* In most cases this operation is a result of transforming operations. When transformation returns
* {@link module:engine/model/operation/nooperation~NoOperation} it means that changes done by the transformed operation
* have already been applied.
*
* @extends module:engine/model/operation/operation~Operation
*/
class NoOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
get type() {
return 'noop';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/nooperation~NoOperation} Clone of this operation.
*/
clone() {
return new NoOperation( this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/nooperation~NoOperation}
*/
getReversed() {
return new NoOperation( this.baseVersion + 1 );
}
_execute() {
}
/**
* @inheritDoc
*/
static get className() {
return 'NoOperation';
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `NoOperation( ${ this.baseVersion } )`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js":
/*!**********************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js ***!
\**********************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Operation; });
/**
* @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 engine/model/operation/operation
*/
/**
* Abstract base operation class.
*
* @abstract
*/
class Operation {
/**
* Base operation constructor.
*
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( baseVersion ) {
/**
* {@link module:engine/model/document~Document#version} on which operation can be applied. If you try to
* {@link module:engine/model/model~Model#applyOperation apply} operation with different base version than the
* {@link module:engine/model/document~Document#version document version} the
* {@link module:utils/ckeditorerror~CKEditorError model-document-applyOperation-wrong-version} error is thrown.
*
* @member {Number}
*/
this.baseVersion = baseVersion;
/**
* Defines whether operation is executed on attached or detached {@link module:engine/model/item~Item items}.
*
* @readonly
* @member {Boolean} #isDocumentOperation
*/
this.isDocumentOperation = this.baseVersion !== null;
/**
* {@link module:engine/model/batch~Batch Batch} to which the operation is added or `null` if the operation is not
* added to any batch yet.
*
* @member {module:engine/model/batch~Batch|null} #batch
*/
this.batch = null;
/**
* Operation type.
*
* @readonly
* @member {String} #type
*/
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @method #clone
* @returns {module:engine/model/operation/operation~Operation} Clone of this operation.
*/
/**
* Creates and returns a reverse operation. Reverse operation when executed right after
* the original operation will bring back tree model state to the point before the original
* operation execution. In other words, it reverses changes done by the original operation.
*
* Keep in mind that tree model state may change since executing the original operation,
* so reverse operation will be "outdated". In that case you will need to transform it by
* all operations that were executed after the original operation.
*
* @method #getReversed
* @returns {module:engine/model/operation/operation~Operation} Reversed operation.
*/
/**
* Executes the operation - modifications described by the operation properties will be applied to the model tree.
*
* @protected
* @method #_execute
*/
}
/**
* Checks whether the operation's parameters are correct and the operation can be correctly executed. Throws
* an error if operation is not valid.
*
* @protected
* @method #_validate
*/
_validate() {
}
/**
* Custom toJSON method to solve child-parent circular dependencies.
*
* @method #toJSON
* @returns {Object} Clone of this object with the operation property replaced with string.
*/
toJSON() {
// This method creates only a shallow copy, all nested objects should be defined separately.
// See https://github.com/ckeditor/ckeditor5-engine/issues/1477.
const json = Object.assign( {}, this );
json.__className = this.constructor.className;
// Remove reference to the parent `Batch` to avoid circular dependencies.
delete json.batch;
// Only document operations are shared with other clients so it is not necessary to keep this information.
delete json.isDocumentOperation;
return json;
}
/**
* Name of the operation class used for serialization.
*
* @type {String}
*/
static get className() {
return 'Operation';
}
/**
* Creates Operation object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} doc Document on which this operation will be applied.
* @returns {module:engine/model/operation/operation~Operation}
*/
static fromJSON( json ) {
return new this( json.baseVersion );
}
// @if CK_DEBUG_ENGINE // log() {
// @if CK_DEBUG_ENGINE // console.log( this.toString() );
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operationfactory.js":
/*!*****************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operationfactory.js ***!
\*****************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return OperationFactory; });
/* harmony import */ var _operation_attributeoperation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../operation/attributeoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/attributeoperation.js");
/* harmony import */ var _operation_insertoperation__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../operation/insertoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/insertoperation.js");
/* harmony import */ var _operation_markeroperation__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../operation/markeroperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/markeroperation.js");
/* harmony import */ var _operation_moveoperation__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../operation/moveoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/moveoperation.js");
/* harmony import */ var _operation_nooperation__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../operation/nooperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/nooperation.js");
/* harmony import */ var _operation_operation__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../operation/operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _operation_renameoperation__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../operation/renameoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/renameoperation.js");
/* harmony import */ var _operation_rootattributeoperation__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../operation/rootattributeoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/rootattributeoperation.js");
/* harmony import */ var _operation_splitoperation__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../operation/splitoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/splitoperation.js");
/* harmony import */ var _operation_mergeoperation__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../operation/mergeoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/mergeoperation.js");
/**
* @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 engine/model/operation/operationfactory
*/
const operations = {};
operations[ _operation_attributeoperation__WEBPACK_IMPORTED_MODULE_0__["default"].className ] = _operation_attributeoperation__WEBPACK_IMPORTED_MODULE_0__["default"];
operations[ _operation_insertoperation__WEBPACK_IMPORTED_MODULE_1__["default"].className ] = _operation_insertoperation__WEBPACK_IMPORTED_MODULE_1__["default"];
operations[ _operation_markeroperation__WEBPACK_IMPORTED_MODULE_2__["default"].className ] = _operation_markeroperation__WEBPACK_IMPORTED_MODULE_2__["default"];
operations[ _operation_moveoperation__WEBPACK_IMPORTED_MODULE_3__["default"].className ] = _operation_moveoperation__WEBPACK_IMPORTED_MODULE_3__["default"];
operations[ _operation_nooperation__WEBPACK_IMPORTED_MODULE_4__["default"].className ] = _operation_nooperation__WEBPACK_IMPORTED_MODULE_4__["default"];
operations[ _operation_operation__WEBPACK_IMPORTED_MODULE_5__["default"].className ] = _operation_operation__WEBPACK_IMPORTED_MODULE_5__["default"];
operations[ _operation_renameoperation__WEBPACK_IMPORTED_MODULE_6__["default"].className ] = _operation_renameoperation__WEBPACK_IMPORTED_MODULE_6__["default"];
operations[ _operation_rootattributeoperation__WEBPACK_IMPORTED_MODULE_7__["default"].className ] = _operation_rootattributeoperation__WEBPACK_IMPORTED_MODULE_7__["default"];
operations[ _operation_splitoperation__WEBPACK_IMPORTED_MODULE_8__["default"].className ] = _operation_splitoperation__WEBPACK_IMPORTED_MODULE_8__["default"];
operations[ _operation_mergeoperation__WEBPACK_IMPORTED_MODULE_9__["default"].className ] = _operation_mergeoperation__WEBPACK_IMPORTED_MODULE_9__["default"];
/**
* A factory class for creating operations.
*
* @abstract
*/
class OperationFactory {
/**
* Creates an operation instance from a JSON object (parsed JSON string).
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/operation~Operation}
*/
static fromJSON( json, document ) {
return operations[ json.__className ].fromJSON( json, document );
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/renameoperation.js":
/*!****************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/renameoperation.js ***!
\****************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return RenameOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _element__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../element */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/**
* @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 engine/model/operation/renameoperation
*/
/**
* Operation to change element's name.
*
* Using this class you can change element's name.
*
* @extends module:engine/model/operation/operation~Operation
*/
class RenameOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates an operation that changes element's name.
*
* @param {module:engine/model/position~Position} position Position before an element to change.
* @param {String} oldName Current name of the element.
* @param {String} newName New name for the element.
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( position, oldName, newName, baseVersion ) {
super( baseVersion );
/**
* Position before an element to change.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/renameoperation~RenameOperation#position
*/
this.position = position;
// This position sticks to the next node because it is a position before the node that we want to change.
this.position.stickiness = 'toNext';
/**
* Current name of the element.
*
* @member {String} module:engine/model/operation/renameoperation~RenameOperation#oldName
*/
this.oldName = oldName;
/**
* New name for the element.
*
* @member {String} module:engine/model/operation/renameoperation~RenameOperation#newName
*/
this.newName = newName;
}
/**
* @inheritDoc
*/
get type() {
return 'rename';
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/renameoperation~RenameOperation} Clone of this operation.
*/
clone() {
return new RenameOperation( this.position.clone(), this.oldName, this.newName, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/renameoperation~RenameOperation}
*/
getReversed() {
return new RenameOperation( this.position.clone(), this.newName, this.oldName, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_validate() {
const element = this.position.nodeAfter;
if ( !( element instanceof _element__WEBPACK_IMPORTED_MODULE_1__["default"] ) ) {
/**
* Given position is invalid or node after it is not instance of Element.
*
* @error rename-operation-wrong-position
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'rename-operation-wrong-position',
this
);
} else if ( element.name !== this.oldName ) {
/**
* Element to change has different name than operation's old name.
*
* @error rename-operation-wrong-name
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'rename-operation-wrong-name',
this
);
}
}
/**
* @inheritDoc
*/
_execute() {
const element = this.position.nodeAfter;
element.name = this.newName;
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
json.position = this.position.toJSON();
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'RenameOperation';
}
/**
* Creates `RenameOperation` object from deserialized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation}
*/
static fromJSON( json, document ) {
return new RenameOperation( _position__WEBPACK_IMPORTED_MODULE_3__["default"].fromJSON( json.position, document ), json.oldName, json.newName, json.baseVersion );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `RenameOperation( ${ this.baseVersion } ): ` +
// @if CK_DEBUG_ENGINE // `${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/rootattributeoperation.js":
/*!***********************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/rootattributeoperation.js ***!
\***********************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return RootAttributeOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/operation/rootattributeoperation
*/
/**
* Operation to change root element's attribute. Using this class you can add, remove or change value of the attribute.
*
* This operation is needed, because root elements can't be changed through
* @link module:engine/model/operation/attributeoperation~AttributeOperation}.
* It is because {@link module:engine/model/operation/attributeoperation~AttributeOperation}
* requires a range to change and root element can't
* be a part of range because every {@link module:engine/model/position~Position} has to be inside a root.
* {@link module:engine/model/position~Position} can't be created before a root element.
*
* @extends module:engine/model/operation/operation~Operation
*/
class RootAttributeOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates an operation that changes, removes or adds attributes on root element.
*
* @see module:engine/model/operation/attributeoperation~AttributeOperation
* @param {module:engine/model/rootelement~RootElement} root Root element to change.
* @param {String} key Key of an attribute to change or remove.
* @param {*} oldValue Old value of the attribute with given key or `null` if adding a new attribute.
* @param {*} newValue New value to set for the attribute. If `null`, then the operation just removes the attribute.
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( root, key, oldValue, newValue, baseVersion ) {
super( baseVersion );
/**
* Root element to change.
*
* @readonly
* @member {module:engine/model/rootelement~RootElement}
*/
this.root = root;
/**
* Key of an attribute to change or remove.
*
* @readonly
* @member {String}
*/
this.key = key;
/**
* Old value of the attribute with given key or `null` if adding a new attribute.
*
* @readonly
* @member {*}
*/
this.oldValue = oldValue;
/**
* New value to set for the attribute. If `null`, then the operation just removes the attribute.
*
* @readonly
* @member {*}
*/
this.newValue = newValue;
}
/**
* @inheritDoc
*/
get type() {
if ( this.oldValue === null ) {
return 'addRootAttribute';
} else if ( this.newValue === null ) {
return 'removeRootAttribute';
} else {
return 'changeRootAttribute';
}
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation} Clone of this operation.
*/
clone() {
return new RootAttributeOperation( this.root, this.key, this.oldValue, this.newValue, this.baseVersion );
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation}
*/
getReversed() {
return new RootAttributeOperation( this.root, this.key, this.newValue, this.oldValue, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_validate() {
if ( this.root != this.root.root || this.root.is( 'documentFragment' ) ) {
/**
* The element to change is not a root element.
*
* @error rootattribute-operation-not-a-root
* @param {module:engine/model/rootelement~RootElement} root
* @param {String} key
* @param {*} value
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"](
'rootattribute-operation-not-a-root',
this,
{ root: this.root, key: this.key }
);
}
if ( this.oldValue !== null && this.root.getAttribute( this.key ) !== this.oldValue ) {
/**
* The attribute which should be removed does not exists for the given node.
*
* @error rootattribute-operation-wrong-old-value
* @param {module:engine/model/rootelement~RootElement} root
* @param {String} key
* @param {*} value
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"](
'rootattribute-operation-wrong-old-value',
this,
{ root: this.root, key: this.key }
);
}
if ( this.oldValue === null && this.newValue !== null && this.root.hasAttribute( this.key ) ) {
/**
* The attribute with given key already exists for the given node.
*
* @error rootattribute-operation-attribute-exists
* @param {module:engine/model/rootelement~RootElement} root
* @param {String} key
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"](
'rootattribute-operation-attribute-exists',
this,
{ root: this.root, key: this.key }
);
}
}
/**
* @inheritDoc
*/
_execute() {
if ( this.newValue !== null ) {
this.root._setAttribute( this.key, this.newValue );
} else {
this.root._removeAttribute( this.key );
}
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
json.root = this.root.toJSON();
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'RootAttributeOperation';
}
/**
* Creates RootAttributeOperation object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/rootattributeoperation~RootAttributeOperation}
*/
static fromJSON( json, document ) {
if ( !document.getRoot( json.root ) ) {
/**
* Cannot create RootAttributeOperation for document. Root with specified name does not exist.
*
* @error rootattribute-operation-fromjson-no-root
* @param {String} rootName
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_1__["default"]( 'rootattribute-operation-fromjson-no-root', this, { rootName: json.root } );
}
return new RootAttributeOperation( document.getRoot( json.root ), json.key, json.oldValue, json.newValue, json.baseVersion );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `RootAttributeOperation( ${ this.baseVersion } ): ` +
// @if CK_DEBUG_ENGINE // `"${ this.key }": ${ JSON.stringify( this.oldValue ) }` +
// @if CK_DEBUG_ENGINE // ` -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/splitoperation.js":
/*!***************************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/splitoperation.js ***!
\***************************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return SplitOperation; });
/* harmony import */ var _operation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./operation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/operation.js");
/* harmony import */ var _mergeoperation__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./mergeoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/mergeoperation.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./utils */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/operation/splitoperation
*/
/**
* Operation to split {@link module:engine/model/element~Element an element} at given
* {@link module:engine/model/operation/splitoperation~SplitOperation#splitPosition split position} into two elements,
* both containing a part of the element's original content.
*
* @extends module:engine/model/operation/operation~Operation
*/
class SplitOperation extends _operation__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates a split operation.
*
* @param {module:engine/model/position~Position} splitPosition Position at which an element should be split.
* @param {Number} howMany Total offset size of elements that are in the split element after `position`.
* @param {module:engine/model/position~Position|null} graveyardPosition Position in the graveyard root before the element which
* should be used as a parent of the nodes after `position`. If it is not set, a copy of the the `position` parent will be used.
* @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
* can be applied or `null` if the operation operates on detached (non-document) tree.
*/
constructor( splitPosition, howMany, graveyardPosition, baseVersion ) {
super( baseVersion );
/**
* Position at which an element should be split.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#splitPosition
*/
this.splitPosition = splitPosition.clone();
// Keep position sticking to the next node. This way any new content added at the place where the element is split
// will be left in the original element.
this.splitPosition.stickiness = 'toNext';
/**
* Total offset size of elements that are in the split element after `position`.
*
* @member {Number} module:engine/model/operation/splitoperation~SplitOperation#howMany
*/
this.howMany = howMany;
/**
* Position at which the clone of split element (or element from graveyard) will be inserted.
*
* @member {module:engine/model/position~Position} module:engine/model/operation/splitoperation~SplitOperation#insertionPosition
*/
this.insertionPosition = SplitOperation.getInsertionPosition( splitPosition );
this.insertionPosition.stickiness = 'toNone';
/**
* Position in the graveyard root before the element which should be used as a parent of the nodes after `position`.
* If it is not set, a copy of the the `position` parent will be used.
*
* The default behavior is to clone the split element. Element from graveyard is used during undo.
*
* @member {module:engine/model/position~Position|null} #graveyardPosition
*/
this.graveyardPosition = graveyardPosition ? graveyardPosition.clone() : null;
if ( this.graveyardPosition ) {
this.graveyardPosition.stickiness = 'toNext';
}
}
/**
* @inheritDoc
*/
get type() {
return 'split';
}
/**
* Position inside the new clone of a split element.
*
* This is a position where nodes that are after the split position will be moved to.
*
* @readonly
* @type {module:engine/model/position~Position}
*/
get moveTargetPosition() {
const path = this.insertionPosition.path.slice();
path.push( 0 );
return new _position__WEBPACK_IMPORTED_MODULE_2__["default"]( this.insertionPosition.root, path );
}
/**
* Artificial range that contains all the nodes from the split element that will be moved to the new element.
* The range starts at {@link ~#splitPosition} and ends in the same parent, at `POSITIVE_INFINITY` offset.
*
* @readonly
* @type {module:engine/model/range~Range}
*/
get movedRange() {
const end = this.splitPosition.getShiftedBy( Number.POSITIVE_INFINITY );
return new _range__WEBPACK_IMPORTED_MODULE_3__["default"]( this.splitPosition, end );
}
/**
* Creates and returns an operation that has the same parameters as this operation.
*
* @returns {module:engine/model/operation/splitoperation~SplitOperation} Clone of this operation.
*/
clone() {
const split = new this.constructor( this.splitPosition, this.howMany, this.graveyardPosition, this.baseVersion );
split.insertionPosition = this.insertionPosition;
return split;
}
/**
* See {@link module:engine/model/operation/operation~Operation#getReversed `Operation#getReversed()`}.
*
* @returns {module:engine/model/operation/mergeoperation~MergeOperation}
*/
getReversed() {
const graveyard = this.splitPosition.root.document.graveyard;
const graveyardPosition = new _position__WEBPACK_IMPORTED_MODULE_2__["default"]( graveyard, [ 0 ] );
return new _mergeoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( this.moveTargetPosition, this.howMany, this.splitPosition, graveyardPosition, this.baseVersion + 1 );
}
/**
* @inheritDoc
*/
_validate() {
const element = this.splitPosition.parent;
const offset = this.splitPosition.offset;
// Validate whether split operation has correct parameters.
if ( !element || element.maxOffset < offset ) {
/**
* Split position is invalid.
*
* @error split-operation-position-invalid
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( 'split-operation-position-invalid', this );
} else if ( !element.parent ) {
/**
* Cannot split root element.
*
* @error split-operation-split-in-root
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( 'split-operation-split-in-root', this );
} else if ( this.howMany != element.maxOffset - this.splitPosition.offset ) {
/**
* Split operation specifies wrong number of nodes to move.
*
* @error split-operation-how-many-invalid
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( 'split-operation-how-many-invalid', this );
} else if ( this.graveyardPosition && !this.graveyardPosition.nodeAfter ) {
/**
* Graveyard position invalid.
*
* @error split-operation-graveyard-position-invalid
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_5__["default"]( 'split-operation-graveyard-position-invalid', this );
}
}
/**
* @inheritDoc
*/
_execute() {
const splitElement = this.splitPosition.parent;
if ( this.graveyardPosition ) {
Object(_utils__WEBPACK_IMPORTED_MODULE_4__["_move"])( _range__WEBPACK_IMPORTED_MODULE_3__["default"]._createFromPositionAndShift( this.graveyardPosition, 1 ), this.insertionPosition );
} else {
const newElement = splitElement._clone();
Object(_utils__WEBPACK_IMPORTED_MODULE_4__["_insert"])( this.insertionPosition, newElement );
}
const sourceRange = new _range__WEBPACK_IMPORTED_MODULE_3__["default"](
_position__WEBPACK_IMPORTED_MODULE_2__["default"]._createAt( splitElement, this.splitPosition.offset ),
_position__WEBPACK_IMPORTED_MODULE_2__["default"]._createAt( splitElement, splitElement.maxOffset )
);
Object(_utils__WEBPACK_IMPORTED_MODULE_4__["_move"])( sourceRange, this.moveTargetPosition );
}
/**
* @inheritDoc
*/
toJSON() {
const json = super.toJSON();
json.splitPosition = this.splitPosition.toJSON();
json.insertionPosition = this.insertionPosition.toJSON();
if ( this.graveyardPosition ) {
json.graveyardPosition = this.graveyardPosition.toJSON();
}
return json;
}
/**
* @inheritDoc
*/
static get className() {
return 'SplitOperation';
}
/**
* Helper function that returns a default insertion position basing on given `splitPosition`. The default insertion
* position is after the split element.
*
* @param {module:engine/model/position~Position} splitPosition
* @returns {module:engine/model/position~Position}
*/
static getInsertionPosition( splitPosition ) {
const path = splitPosition.path.slice( 0, -1 );
path[ path.length - 1 ]++;
return new _position__WEBPACK_IMPORTED_MODULE_2__["default"]( splitPosition.root, path );
}
/**
* Creates `SplitOperation` object from deserilized object, i.e. from parsed JSON string.
*
* @param {Object} json Deserialized JSON object.
* @param {module:engine/model/document~Document} document Document on which this operation will be applied.
* @returns {module:engine/model/operation/splitoperation~SplitOperation}
*/
static fromJSON( json, document ) {
const splitPosition = _position__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( json.splitPosition, document );
const insertionPosition = _position__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( json.insertionPosition, document );
const graveyardPosition = json.graveyardPosition ? _position__WEBPACK_IMPORTED_MODULE_2__["default"].fromJSON( json.graveyardPosition, document ) : null;
const split = new this( splitPosition, json.howMany, graveyardPosition, json.baseVersion );
split.insertionPosition = insertionPosition;
return split;
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `SplitOperation( ${ this.baseVersion } ): ${ this.splitPosition } ` +
// @if CK_DEBUG_ENGINE // `( ${ this.howMany } ) -> ${ this.insertionPosition }` +
// @if CK_DEBUG_ENGINE // `${ this.graveyardPosition ? ' with ' + this.graveyardPosition : '' }`;
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/transform.js":
/*!**********************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/transform.js ***!
\**********************************************************************************/
/*! exports provided: transform, transformSets */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "transform", function() { return transform; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "transformSets", function() { return transformSets; });
/* harmony import */ var _insertoperation__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./insertoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/insertoperation.js");
/* harmony import */ var _attributeoperation__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./attributeoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/attributeoperation.js");
/* harmony import */ var _renameoperation__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./renameoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/renameoperation.js");
/* harmony import */ var _markeroperation__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./markeroperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/markeroperation.js");
/* harmony import */ var _moveoperation__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./moveoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/moveoperation.js");
/* harmony import */ var _rootattributeoperation__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./rootattributeoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/rootattributeoperation.js");
/* harmony import */ var _mergeoperation__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./mergeoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/mergeoperation.js");
/* harmony import */ var _splitoperation__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./splitoperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/splitoperation.js");
/* harmony import */ var _nooperation__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./nooperation */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/nooperation.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/comparearrays */ "./node_modules/@ckeditor/ckeditor5-utils/src/comparearrays.js");
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
const transformations = new Map();
/**
* @module engine/model/operation/transform
*/
/**
* Sets a transformation function to be be used to transform instances of class `OperationA` by instances of class `OperationB`.
*
* The `transformationFunction` is passed three parameters:
*
* * `a` - operation to be transformed, an instance of `OperationA`,
* * `b` - operation to be transformed by, an instance of `OperationB`,
* * {@link module:engine/model/operation/transform~TransformationContext `context`} - object with additional information about
* transformation context.
*
* The `transformationFunction` should return transformation result, which is an array with one or multiple
* {@link module:engine/model/operation/operation~Operation operation} instances.
*
* @protected
* @param {Function} OperationA
* @param {Function} OperationB
* @param {Function} transformationFunction Function to use for transforming.
*/
function setTransformation( OperationA, OperationB, transformationFunction ) {
let aGroup = transformations.get( OperationA );
if ( !aGroup ) {
aGroup = new Map();
transformations.set( OperationA, aGroup );
}
aGroup.set( OperationB, transformationFunction );
}
/**
* Returns a previously set transformation function for transforming an instance of `OperationA` by an instance of `OperationB`.
*
* If no transformation was set for given pair of operations, {@link module:engine/model/operation/transform~noUpdateTransformation}
* is returned. This means that if no transformation was set, the `OperationA` instance will not change when transformed
* by the `OperationB` instance.
*
* @private
* @param {Function} OperationA
* @param {Function} OperationB
* @returns {Function} Function set to transform an instance of `OperationA` by an instance of `OperationB`.
*/
function getTransformation( OperationA, OperationB ) {
const aGroup = transformations.get( OperationA );
if ( aGroup && aGroup.has( OperationB ) ) {
return aGroup.get( OperationB );
}
return noUpdateTransformation;
}
/**
* A transformation function that only clones operation to transform, without changing it.
*
* @private
* @param {module:engine/model/operation/operation~Operation} a Operation to transform.
* @returns {Array.}
*/
function noUpdateTransformation( a ) {
return [ a ];
}
/**
* Transforms operation `a` by operation `b`.
*
* @param {module:engine/model/operation/operation~Operation} a Operation to be transformed.
* @param {module:engine/model/operation/operation~Operation} b Operation to transform by.
* @param {module:engine/model/operation/transform~TransformationContext} context Transformation context for this transformation.
* @returns {Array.} Transformation result.
*/
function transform( a, b, context = {} ) {
const transformationFunction = getTransformation( a.constructor, b.constructor );
/* eslint-disable no-useless-catch */
try {
a = a.clone();
return transformationFunction( a, b, context );
} catch ( e ) {
// @if CK_DEBUG // console.warn( 'Error during operation transformation!', e.message );
// @if CK_DEBUG // console.warn( 'Transformed operation', a );
// @if CK_DEBUG // console.warn( 'Operation transformed by', b );
// @if CK_DEBUG // console.warn( 'context.aIsStrong', context.aIsStrong );
// @if CK_DEBUG // console.warn( 'context.aWasUndone', context.aWasUndone );
// @if CK_DEBUG // console.warn( 'context.bWasUndone', context.bWasUndone );
// @if CK_DEBUG // console.warn( 'context.abRelation', context.abRelation );
// @if CK_DEBUG // console.warn( 'context.baRelation', context.baRelation );
throw e;
}
/* eslint-enable no-useless-catch */
}
/**
* Performs a transformation of two sets of operations - `operationsA` and `operationsB`. The transformation is two-way -
* both transformed `operationsA` and transformed `operationsB` are returned.
*
* Note, that the first operation in each set should base on the same document state (
* {@link module:engine/model/document~Document#version document version}).
*
* It is assumed that `operationsA` are "more important" during conflict resolution between two operations.
*
* New copies of both passed arrays and operations inside them are returned. Passed arguments are not altered.
*
* Base versions of the transformed operations sets are updated accordingly. For example, assume that base versions are `4`
* and there are `3` operations in `operationsA` and `5` operations in `operationsB`. Then:
*
* * transformed `operationsA` will start from base version `9` (`4` base version + `5` operations B),
* * transformed `operationsB` will start from base version `7` (`4` base version + `3` operations A).
*
* If no operation was broken into two during transformation, then both sets will end up with an operation that bases on version `11`:
*
* * transformed `operationsA` start from `9` and there are `3` of them, so the last will have `baseVersion` equal to `11`,
* * transformed `operationsB` start from `7` and there are `5` of them, so the last will have `baseVersion` equal to `11`.
*
* @param {Array.} operationsA
* @param {Array.} operationsB
* @param {Object} options Additional transformation options.
* @param {module:engine/model/document~Document|null} options.document Document which the operations change.
* @param {Boolean} [options.useRelations=false] Whether during transformation relations should be used (used during undo for
* better conflict resolution).
* @param {Boolean} [options.padWithNoOps=false] Whether additional {@link module:engine/model/operation/nooperation~NoOperation}s
* should be added to the transformation results to force the same last base version for both transformed sets (in case
* if some operations got broken into multiple operations during transformation).
* @returns {Object} Transformation result.
* @returns {Array.} return.operationsA Transformed `operationsA`.
* @returns {Array.} return.operationsB Transformed `operationsB`.
* @returns {Map} return.originalOperations A map that links transformed operations to original operations. The keys are the transformed
* operations and the values are the original operations from the input (`operationsA` and `operationsB`).
*/
function transformSets( operationsA, operationsB, options ) {
// Create new arrays so the originally passed arguments are not changed.
// No need to clone operations, they are cloned as they are transformed.
operationsA = operationsA.slice();
operationsB = operationsB.slice();
const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
contextFactory.setOriginalOperations( operationsA );
contextFactory.setOriginalOperations( operationsB );
const originalOperations = contextFactory.originalOperations;
// If one of sets is empty there is simply nothing to transform, so return sets as they are.
if ( operationsA.length == 0 || operationsB.length == 0 ) {
return { operationsA, operationsB, originalOperations };
}
//
// Following is a description of transformation process:
//
// There are `operationsA` and `operationsB` to be transformed, both by both.
//
// So, suppose we have sets of two operations each: `operationsA` = `[ a1, a2 ]`, `operationsB` = `[ b1, b2 ]`.
//
// Remember, that we can only transform operations that base on the same context. We assert that `a1` and `b1` base on
// the same context and we transform them. Then, we get `a1'` and `b1'`. `a2` bases on a context with `a1` -- `a2`
// is an operation that followed `a1`. Similarly, `b2` bases on a context with `b1`.
//
// However, since `a1'` is a result of transformation by `b1`, `a1'` now also has a context with `b1`. This means that
// we can safely transform `a1'` by `b2`. As we finish transforming `a1`, we also transformed all `operationsB`.
// All `operationsB` also have context including `a1`. Now, we can properly transform `a2` by those operations.
//
// The transformation process can be visualized on a transformation diagram ("diamond diagram"):
//
// [the initial state]
// [common for a1 and b1]
//
// *
// / \
// / \
// b1 a1
// / \
// / \
// * *
// / \ / \
// / \ / \
// b2 a1' b1' a2
// / \ / \
// / \ / \
// * * *
// \ / \ /
// \ / \ /
// a1'' b2' a2' b1''
// \ / \ /
// \ / \ /
// * *
// \ /
// \ /
// a2'' b2''
// \ /
// \ /
// *
//
// [the final state]
//
// The final state can be reached from the initial state by applying `a1`, `a2`, `b1''` and `b2''`, as well as by
// applying `b1`, `b2`, `a1''`, `a2''`. Note how the operations get to a proper common state before each pair is
// transformed.
//
// Another thing to consider is that an operation during transformation can be broken into multiple operations.
// Suppose that `a1` * `b1` = `[ a11', a12' ]` (instead of `a1'` that we considered previously).
//
// In that case, we leave `a12'` for later and we continue transforming `a11'` until it is transformed by all `operationsB`
// (in our case it is just `b2`). At this point, `b1` is transformed by "whole" `a1`, while `b2` is only transformed
// by `a11'`. Similarly, `a12'` is only transformed by `b1`. This leads to a conclusion that we need to start transforming `a12'`
// from the moment just after it was broken. So, `a12'` is transformed by `b2`. Now, "the whole" `a1` is transformed
// by `operationsB`, while all `operationsB` are transformed by "the whole" `a1`. This means that we can continue with
// following `operationsA` (in our case it is just `a2`).
//
// Of course, also `operationsB` can be broken. However, since we focus on transforming operation `a` to the end,
// the only thing to do is to store both pieces of operation `b`, so that the next transformed operation `a` will
// be transformed by both of them.
//
// *
// / \
// / \
// / \
// b1 a1
// / \
// / \
// / \
// * *
// / \ / \
// / a11' / \
// / \ / \
// b2 * b1' a2
// / / \ / \
// / / a12' / \
// / / \ / \
// * b2' * *
// \ / / \ /
// a11'' / b21'' \ /
// \ / / \ /
// * * a2' b1''
// \ / \ \ /
// a12'' b22''\ \ /
// \ / \ \ /
// * a2'' *
// \ \ /
// \ \ b21'''
// \ \ /
// a2''' *
// \ /
// \ b22'''
// \ /
// *
//
// Note, how `a1` is broken and transformed into `a11'` and `a12'`, while `b2'` got broken and transformed into `b21''` and `b22''`.
//
// Having all that on mind, here is an outline for the transformation process algorithm:
//
// 1. We have `operationsA` and `operationsB` array, which we dynamically update as the transformation process goes.
//
// 2. We take next (or first) operation from `operationsA` and check from which operation `b` we need to start transforming it.
// All original `operationsA` are set to be transformed starting from the first operation `b`.
//
// 3. We take operations from `operationsB`, one by one, starting from the correct one, and transform operation `a`
// by operation `b` (and vice versa). We update `operationsA` and `operationsB` by replacing the original operations
// with the transformation results.
//
// 4. If operation is broken into multiple operations, we save all the new operations in the place of the
// original operation.
//
// 5. Additionally, if operation `a` was broken, for the "new" operation, we remember from which operation `b` it should
// be transformed by.
//
// 6. We continue transforming "current" operation `a` until it is transformed by all `operationsB`. Then, go to 2.
// unless the last operation `a` was transformed.
//
// The actual implementation of the above algorithm is slightly different, as only one loop (while) is used.
// The difference is that we have "current" `a` operation to transform and we store the index of the next `b` operation
// to transform by. Each loop operates on two indexes then: index pointing to currently processed `a` operation and
// index pointing to next `b` operation. Each loop is just one `a * b` + `b * a` transformation. After each loop
// operation `b` index is updated. If all `b` operations were visited for the current `a` operation, we change
// current `a` operation index to the next one.
//
// For each operation `a`, keeps information what is the index in `operationsB` from which the transformation should start.
const nextTransformIndex = new WeakMap();
// For all the original `operationsA`, set that they should be transformed starting from the first of `operationsB`.
for ( const op of operationsA ) {
nextTransformIndex.set( op, 0 );
}
// Additional data that is used for some postprocessing after the main transformation process is done.
const data = {
nextBaseVersionA: operationsA[ operationsA.length - 1 ].baseVersion + 1,
nextBaseVersionB: operationsB[ operationsB.length - 1 ].baseVersion + 1,
originalOperationsACount: operationsA.length,
originalOperationsBCount: operationsB.length
};
// Index of currently transformed operation `a`.
let i = 0;
// While not all `operationsA` are transformed...
while ( i < operationsA.length ) {
// Get "current" operation `a`.
const opA = operationsA[ i ];
// For the "current" operation `a`, get the index of the next operation `b` to transform by.
const indexB = nextTransformIndex.get( opA );
// If operation `a` was already transformed by every operation `b`, change "current" operation `a` to the next one.
if ( indexB == operationsB.length ) {
i++;
continue;
}
const opB = operationsB[ indexB ];
// Transform `a` by `b` and `b` by `a`.
const newOpsA = transform( opA, opB, contextFactory.getContext( opA, opB, true ) );
const newOpsB = transform( opB, opA, contextFactory.getContext( opB, opA, false ) );
// As a result we get one or more `newOpsA` and one or more `newOpsB` operations.
// Update contextual information about operations.
contextFactory.updateRelation( opA, opB );
contextFactory.setOriginalOperations( newOpsA, opA );
contextFactory.setOriginalOperations( newOpsB, opB );
// For new `a` operations, update their index of the next operation `b` to transform them by.
//
// This is needed even if there was only one result (`a` was not broken) because that information is used
// at the beginning of this loop every time.
for ( const newOpA of newOpsA ) {
// Acknowledge, that operation `b` also might be broken into multiple operations.
//
// This is why we raise `indexB` not just by 1. If `newOpsB` are multiple operations, they will be
// spliced in the place of `opB`. So we need to change `transformBy` accordingly, so that an operation won't
// be transformed by the same operation (part of it) again.
nextTransformIndex.set( newOpA, indexB + newOpsB.length );
}
// Update `operationsA` and `operationsB` with the transformed versions.
operationsA.splice( i, 1, ...newOpsA );
operationsB.splice( indexB, 1, ...newOpsB );
}
if ( options.padWithNoOps ) {
// If no-operations padding is enabled, count how many extra `a` and `b` operations were generated.
const brokenOperationsACount = operationsA.length - data.originalOperationsACount;
const brokenOperationsBCount = operationsB.length - data.originalOperationsBCount;
// Then, if that number is not the same, pad `operationsA` or `operationsB` with correct number of no-ops so
// that the base versions are equalled.
//
// Note that only one array will be updated, as only one of those subtractions can be greater than zero.
padWithNoOps( operationsA, brokenOperationsBCount - brokenOperationsACount );
padWithNoOps( operationsB, brokenOperationsACount - brokenOperationsBCount );
}
// Finally, update base versions of transformed operations.
updateBaseVersions( operationsA, data.nextBaseVersionB );
updateBaseVersions( operationsB, data.nextBaseVersionA );
return { operationsA, operationsB, originalOperations };
}
// Gathers additional data about operations processed during transformation. Can be used to obtain contextual information
// about two operations that are about to be transformed. This contextual information can be used for better conflict resolution.
class ContextFactory {
// Creates `ContextFactory` instance.
//
// @param {module:engine/model/document~Document} document Document which the operations change.
// @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
// better conflict resolution).
// @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
// so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
constructor( document, useRelations, forceWeakRemove = false ) {
// For each operation that is created during transformation process, we keep a reference to the original operation
// which it comes from. The original operation works as a kind of "identifier". Every contextual information
// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
// This way no matter if operation `a` is cloned, then transformed, even breaks, we still have access to the previously
// gathered data through original operation reference.
this.originalOperations = new Map();
// `model.History` instance which information about undone operations will be taken from.
this._history = document.history;
// Whether additional context should be used.
this._useRelations = useRelations;
this._forceWeakRemove = !!forceWeakRemove;
// Relations is a double-map structure (maps in map) where for two operations we store how those operations were related
// to each other. Those relations are evaluated during transformation process. For every transformated pair of operations
// we keep relations between them.
this._relations = new Map();
}
// Sets "original operation" for given operations.
//
// During transformation process, operations are cloned, then changed, then processed again, sometimes broken into two
// or multiple operations. When gathering additional data it is important that all operations can be somehow linked
// so a cloned and transformed "version" still kept track of the data assigned earlier to it.
//
// The original operation object will be used as such an universal linking id. Throughout the transformation process
// all cloned operations will refer to "the original operation" when storing and reading additional data.
//
// If `takeFrom` is not set, each operation from `operations` array will be assigned itself as "the original operation".
// This should be used as an initialization step.
//
// If `takeFrom` is set, each operation from `operations` will be assigned the same original operation as assigned
// for `takeFrom` operation. This should be used to update original operations. It should be used in a way that
// `operations` are the result of `takeFrom` transformation to ensure proper "original operation propagation".
//
// @param {Array.} operations
// @param {module:engine/model/operation/operation~Operation|null} [takeFrom=null]
setOriginalOperations( operations, takeFrom = null ) {
const originalOperation = takeFrom ? this.originalOperations.get( takeFrom ) : null;
for ( const operation of operations ) {
this.originalOperations.set( operation, originalOperation || operation );
}
}
// Saves a relation between operations `opA` and `opB`.
//
// Relations are then later used to help solve conflicts when operations are transformed.
//
// @param {module:engine/model/operation/operation~Operation} opA
// @param {module:engine/model/operation/operation~Operation} opB
updateRelation( opA, opB ) {
// The use of relations is described in a bigger detail in transformation functions.
//
// In brief, this function, for specified pairs of operation types, checks how positions defined in those operations relate.
// Then those relations are saved. For example, for two move operations, it is saved if one of those operations target
// position is before the other operation source position. This kind of information gives contextual information when
// transformation is used during undo. Similar checks are done for other pairs of operations.
//
switch ( opA.constructor ) {
case _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]: {
switch ( opB.constructor ) {
case _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"]: {
if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
this._setRelation( opA, opB, 'insertAtSource' );
} else if ( opA.targetPosition.isEqual( opB.deletionPosition ) ) {
this._setRelation( opA, opB, 'insertBetween' );
} else if ( opA.targetPosition.isAfter( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'moveTargetAfter' );
}
break;
}
case _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]: {
if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opA.targetPosition.isBefore( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'insertBefore' );
} else {
this._setRelation( opA, opB, 'insertAfter' );
}
break;
}
}
break;
}
case _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"]: {
switch ( opB.constructor ) {
case _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"]: {
if ( opA.splitPosition.isBefore( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'splitBefore' );
}
break;
}
case _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]: {
if ( opA.splitPosition.isEqual( opB.sourcePosition ) || opA.splitPosition.isBefore( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'splitBefore' );
}
break;
}
}
break;
}
case _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"]: {
switch ( opB.constructor ) {
case _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"]: {
if ( !opA.targetPosition.isEqual( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'mergeTargetNotMoved' );
}
if ( opA.sourcePosition.isEqual( opB.targetPosition ) ) {
this._setRelation( opA, opB, 'mergeSourceNotMoved' );
}
if ( opA.sourcePosition.isEqual( opB.sourcePosition ) ) {
this._setRelation( opA, opB, 'mergeSameElement' );
}
break;
}
case _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"]: {
if ( opA.sourcePosition.isEqual( opB.splitPosition ) ) {
this._setRelation( opA, opB, 'splitAtSource' );
}
}
}
break;
}
case _markeroperation__WEBPACK_IMPORTED_MODULE_3__["default"]: {
const markerRange = opA.newRange;
if ( !markerRange ) {
return;
}
switch ( opB.constructor ) {
case _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]: {
const movedRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._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',
path: affectedLeft ? markerRange.start.path.slice() : markerRange.end.path.slice()
} );
}
break;
}
case _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"]: {
const wasInLeftElement = markerRange.start.isEqual( opB.targetPosition );
const wasStartBeforeMergedElement = markerRange.start.isEqual( opB.deletionPosition );
const wasEndBeforeMergedElement = markerRange.end.isEqual( opB.deletionPosition );
const wasInRightElement = markerRange.end.isEqual( opB.sourcePosition );
if ( wasInLeftElement || wasStartBeforeMergedElement || wasEndBeforeMergedElement || wasInRightElement ) {
this._setRelation( opA, opB, {
wasInLeftElement,
wasStartBeforeMergedElement,
wasEndBeforeMergedElement,
wasInRightElement
} );
}
break;
}
}
break;
}
}
}
// Evaluates and returns contextual information about two given operations `opA` and `opB` which are about to be transformed.
//
// @param {module:engine/model/operation/operation~Operation} opA
// @param {module:engine/model/operation/operation~Operation} opB
// @returns {module:engine/model/operation/transform~TransformationContext}
getContext( opA, opB, aIsStrong ) {
return {
aIsStrong,
aWasUndone: this._wasUndone( opA ),
bWasUndone: this._wasUndone( opB ),
abRelation: this._useRelations ? this._getRelation( opA, opB ) : null,
baRelation: this._useRelations ? this._getRelation( opB, opA ) : null,
forceWeakRemove: this._forceWeakRemove
};
}
// Returns whether given operation `op` has already been undone.
//
// Information whether an operation was undone gives more context when making a decision when two operations are in conflict.
//
// @param {module:engine/model/operation/operation~Operation} op
// @returns {Boolean}
_wasUndone( op ) {
// For `op`, get its original operation. After all, if `op` is a clone (or even transformed clone) of another
// operation, literally `op` couldn't be undone. It was just generated. If anything, it was the operation it origins
// from which was undone. So get that original operation.
const originalOp = this.originalOperations.get( op );
// And check with the document if the original operation was undone.
return originalOp.wasUndone || this._history.isUndoneOperation( originalOp );
}
// Returns a relation between `opA` and an operation which is undone by `opB`. This can be `String` value if a relation
// was set earlier or `null` if there was no relation between those operations.
//
// This is a little tricky to understand, so let's compare it to `ContextFactory#_wasUndone`.
//
// When `wasUndone( opB )` is used, we check if the `opB` has already been undone. It is obvious, that the
// undoing operation must happen after the undone operation. So, essentially, we have `opB`, we take document history,
// we look forward in the future and ask if in that future `opB` was undone.
//
// Relations is a backward process to `wasUndone()`.
//
// Long story short - using relations is asking what happened in the past. Looking back. This time we have an undoing
// operation `opB` which has undone some other operation. When there is a transformation `opA` x `opB` and there is
// a conflict to solve and `opB` is an undoing operation, we can look back in the history and see what was a relation
// between `opA` and the operation which `opB` undone. Basing on that relation from the past, we can now make
// a better decision when resolving a conflict between two operations, because we know more about the context of
// those two operations.
//
// This is why this function does not return a relation directly between `opA` and `opB` because we need to look
// back to search for a meaningful contextual information.
//
// @param {module:engine/model/operation/operation~Operation} opA
// @param {module:engine/model/operation/operation~Operation} opB
// @returns {String|null}
_getRelation( opA, opB ) {
// Get the original operation. Similarly as in `wasUndone()` it is used as an universal identifier for stored data.
const origB = this.originalOperations.get( opB );
const undoneB = this._history.getUndoneOperation( origB );
// If `opB` is not undoing any operation, there is no relation.
if ( !undoneB ) {
return null;
}
const origA = this.originalOperations.get( opA );
const relationsA = this._relations.get( origA );
// Get all relations for `opA`, and check if there is a relation with `opB`-undone-counterpart. If so, return it.
if ( relationsA ) {
return relationsA.get( undoneB ) || null;
}
return null;
}
// Helper function for `ContextFactory#updateRelations`.
//
// @private
// @param {module:engine/model/operation/operation~Operation} opA
// @param {module:engine/model/operation/operation~Operation} opB
// @param {String} relation
_setRelation( opA, opB, relation ) {
// As always, setting is for original operations, not the clones/transformed operations.
const origA = this.originalOperations.get( opA );
const origB = this.originalOperations.get( opB );
let relationsA = this._relations.get( origA );
if ( !relationsA ) {
relationsA = new Map();
this._relations.set( origA, relationsA );
}
relationsA.set( origB, relation );
}
}
/**
* Holds additional contextual information about a transformed pair of operations (`a` and `b`). Those information
* can be used for better conflict resolving.
*
* @typedef {Object} module:engine/model/operation/transform~TransformationContext
*
* @property {Boolean} aIsStrong Whether `a` is strong operation in this transformation, or weak.
* @property {Boolean} aWasUndone Whether `a` operation was undone.
* @property {Boolean} bWasUndone Whether `b` operation was undone.
* @property {String|null} abRelation The relation between `a` operation and an operation undone by `b` operation.
* @property {String|null} baRelation The relation between `b` operation and an operation undone by `a` operation.
*/
/**
* An utility function that updates {@link module:engine/model/operation/operation~Operation#baseVersion base versions}
* of passed operations.
*
* The function simply sets `baseVersion` as a base version of the first passed operation and then increments it for
* each following operation in `operations`.
*
* @private
* @param {Array.} operations Operations to update.
* @param {Number} baseVersion Base version to set for the first operation in `operations`.
*/
function updateBaseVersions( operations, baseVersion ) {
for ( const operation of operations ) {
operation.baseVersion = baseVersion++;
}
}
/**
* Adds `howMany` instances of {@link module:engine/model/operation/nooperation~NoOperation} to `operations` set.
*
* @private
* @param {Array.} operations
* @param {Number} howMany
*/
function padWithNoOps( operations, howMany ) {
for ( let i = 0; i < howMany; i++ ) {
operations.push( new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) );
}
}
// -----------------------
setTransformation( _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"], _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"], ( a, b, context ) => {
// If operations in conflict, check if their ranges intersect and manage them properly.
//
// Operations can be in conflict only if:
//
// * their key is the same (they change the same attribute), and
// * they are in the same parent (operations for ranges [ 1 ] - [ 3 ] and [ 2, 0 ] - [ 2, 5 ] change different
// elements and can't be in conflict).
if ( a.key === b.key && a.range.start.hasSameParentAs( b.range.start ) ) {
// First, we want to apply change to the part of a range that has not been changed by the other operation.
const operations = a.range.getDifference( b.range ).map( range => {
return new _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( range, a.key, a.oldValue, a.newValue, 0 );
} );
// Then we take care of the common part of ranges.
const common = a.range.getIntersection( b.range );
if ( common ) {
// If this operation is more important, we also want to apply change to the part of the
// original range that has already been changed by the other operation. Since that range
// got changed we also have to update `oldValue`.
if ( context.aIsStrong ) {
operations.push( new _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( common, b.key, b.newValue, a.newValue, 0 ) );
}
}
if ( operations.length == 0 ) {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
return operations;
} else {
// If operations don't conflict, simply return an array containing just a clone of this operation.
return [ a ];
}
} );
setTransformation( _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"], _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], ( a, b ) => {
// Case 1:
//
// The attribute operation range includes the position where nodes were inserted.
// There are two possible scenarios: the inserted nodes were text and they should receive attributes or
// the inserted nodes were elements and they should not receive attributes.
//
if ( a.range.start.hasSameParentAs( b.position ) && a.range.containsPosition( b.position ) ) {
// If new nodes should not receive attributes, two separated ranges will be returned.
// Otherwise, one expanded range will be returned.
const range = a.range._getTransformedByInsertion( b.position, b.howMany, !b.shouldReceiveAttributes );
const result = range.map( r => {
return new _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( r, a.key, a.oldValue, a.newValue, a.baseVersion );
} );
if ( b.shouldReceiveAttributes ) {
// `AttributeOperation#range` includes some newly inserted text.
// The operation should also change the attribute of that text. An example:
//
// Bold should be applied on the following range:
// Fo[zb]ar
//
// In meantime, new text is typed:
// Fozxxbar
//
// Bold should be applied also on the new text:
// Fo[zxxb]ar
// Fo<$text bold="true">zxxb$text>ar
//
// There is a special case to consider here to consider.
//
// Consider setting an attribute with multiple possible values, for example `highlight`. The inserted text might
// have already an attribute value applied and the `oldValue` property of the attribute operation might be wrong:
//
// Attribute `highlight="yellow"` should be applied on the following range:
// Fo[zb]ar
//
// In meantime, character `x` with `highlight="red"` is typed:
//
Fo[z<$text highlight="red">x$text>b]ar
//
// In this case we cannot simply apply operation changing the attribute value from `null` to `"yellow"` for the whole range
// because that would lead to an exception (`oldValue` is incorrect for `x`).
//
// We also cannot break the original range as this would mess up a scenario when there are multiple following
// insert operations, because then only the first inserted character is included in those ranges:
// Fo[z][x][b]ar
--> Fo[z][x]x[b]ar
--> Fo[z][x]xx[b]ar
//
// So, the attribute range needs be expanded, no matter what attributes are set on the inserted nodes:
//
// Fo[z<$text highlight="red">x$text>b]ar
<--- Change from `null` to `yellow`, throwing an exception.
//
// But before that operation would be applied, we will add an additional attribute operation that will change
// attributes on the inserted nodes in a way which would make the original operation correct:
//
// Fo[z{<$text highlight="red">}x$text>b]ar
<--- Change range `{}` from `red` to `null`.
// Fo[zxb]ar
<--- Now change from `null` to `yellow` is completely fine.
//
// Generate complementary attribute operation. Be sure to add it before the original operation.
const op = _getComplementaryAttributeOperations( b, a.key, a.oldValue );
if ( op ) {
result.unshift( op );
}
}
// If nodes should not receive new attribute, we are done here.
return result;
}
// If insert operation is not expanding the attribute operation range, simply transform the range.
a.range = a.range._getTransformedByInsertion( b.position, b.howMany, false )[ 0 ];
return [ a ];
} );
/**
* Helper function for `AttributeOperation` x `InsertOperation` (and reverse) transformation.
*
* For given `insertOperation` it checks the inserted node if it has an attribute `key` set to a value different
* than `newValue`. If so, it generates an `AttributeOperation` which changes the value of `key` attribute to `newValue`.
*
* @private
* @param {module:engine/model/operation/insertoperation~InsertOperation} insertOperation
* @param {String} key
* @param {*} newValue
* @returns {module:engine/model/operation/attributeoperation~AttributeOperation|null}
*/
function _getComplementaryAttributeOperations( insertOperation, key, newValue ) {
const nodes = insertOperation.nodes;
// At the beginning we store the attribute value from the first node.
const insertValue = nodes.getNode( 0 ).getAttribute( key );
if ( insertValue == newValue ) {
return null;
}
const range = new _range__WEBPACK_IMPORTED_MODULE_9__["default"]( insertOperation.position, insertOperation.position.getShiftedBy( insertOperation.howMany ) );
return new _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( range, key, insertValue, newValue, 0 );
}
setTransformation( _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"], _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], ( a, b ) => {
const ranges = [];
// Case 1:
//
// Attribute change on the merged element. In this case, the merged element was moved to the graveyard.
// An additional attribute operation that will change the (re)moved element needs to be generated.
//
if ( a.range.start.hasSameParentAs( b.deletionPosition ) ) {
if ( a.range.containsPosition( b.deletionPosition ) || a.range.start.isEqual( b.deletionPosition ) ) {
ranges.push( _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( b.graveyardPosition, 1 ) );
}
}
const range = a.range._getTransformedByMergeOperation( b );
// Do not add empty (collapsed) ranges to the result. `range` may be collapsed if it contained only the merged element.
if ( !range.isCollapsed ) {
ranges.push( range );
}
// Create `AttributeOperation`s out of the ranges.
return ranges.map( range => {
return new _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( range, a.key, a.oldValue, a.newValue, a.baseVersion );
} );
} );
setTransformation( _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"], _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], ( a, b ) => {
const ranges = _breakRangeByMoveOperation( a.range, b );
// Create `AttributeOperation`s out of the ranges.
return ranges.map( range => new _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"]( range, a.key, a.oldValue, a.newValue, a.baseVersion ) );
} );
// Helper function for `AttributeOperation` x `MoveOperation` transformation.
//
// Takes the passed `range` and transforms it by move operation `moveOp` in a specific way. Only top-level nodes of `range`
// are considered to be in the range. If move operation moves nodes deep from inside of the range, those nodes won't
// be included in the result. In other words, top-level nodes of the ranges from the result are exactly the same as
// top-level nodes of the original `range`.
//
// This is important for `AttributeOperation` because, for its range, it changes only the top-level nodes. So we need to
// track only how those nodes have been affected by `MoveOperation`.
//
// @private
// @param {module:engine/model/range~Range} range
// @param {module:engine/model/operation/moveoperation~MoveOperation} moveOp
// @returns {Array.}
function _breakRangeByMoveOperation( range, moveOp ) {
const moveRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany );
// We are transforming `range` (original range) by `moveRange` (range moved by move operation). As usual when it comes to
// transforming a ranges, we may have a common part of the ranges and we may have a difference part (zero to two ranges).
let common = null;
let difference = [];
// Let's compare the ranges.
if ( moveRange.containsRange( range, true ) ) {
// If the whole original range is moved, treat it whole as a common part. There's also no difference part.
common = range;
} else if ( range.start.hasSameParentAs( moveRange.start ) ) {
// If the ranges are "on the same level" (in the same parent) then move operation may move exactly those nodes
// that are changed by the attribute operation. In this case we get common part and difference part in the usual way.
difference = range.getDifference( moveRange );
common = range.getIntersection( moveRange );
} else {
// In any other situation we assume that original range is different than move range, that is that move operation
// moves other nodes that attribute operation change. Even if the moved range is deep inside in the original range.
//
// Note that this is different than in `.getIntersection` (we would get a common part in that case) and different
// than `.getDifference` (we would get two ranges).
difference = [ range ];
}
const result = [];
// The default behaviour of `_getTransformedByMove` might get wrong results for difference part, though, so
// we do it by hand.
for ( let diff of difference ) {
// First, transform the range by removing moved nodes. Since this is a difference, this is safe, `null` won't be returned
// as the range is different than the moved range.
diff = diff._getTransformedByDeletion( moveOp.sourcePosition, moveOp.howMany );
// Transform also `targetPosition`.
const targetPosition = moveOp.getMovedRangeStart();
// Spread the range only if moved nodes are inserted only between the top-level nodes of the `diff` range.
const spread = diff.start.hasSameParentAs( targetPosition );
// Transform by insertion of moved nodes.
diff = diff._getTransformedByInsertion( targetPosition, moveOp.howMany, spread );
result.push( ...diff );
}
// Common part can be simply transformed by the move operation. This is because move operation will not target to
// that common part (the operation would have to target inside its own moved range).
if ( common ) {
result.push(
common._getTransformedByMove( moveOp.sourcePosition, moveOp.targetPosition, moveOp.howMany, false )[ 0 ]
);
}
return result;
}
setTransformation( _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"], _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], ( a, b ) => {
// Case 1:
//
// Split node is the last node in `AttributeOperation#range`.
// `AttributeOperation#range` needs to be expanded to include the new (split) node.
//
// Attribute `type` to be changed to `numbered` but the `listItem` is split.
// foobar
//
// After split:
// foo bar
//
// After attribute change:
// foo foo
//
if ( a.range.end.isEqual( b.insertionPosition ) ) {
if ( !b.graveyardPosition ) {
a.range.end.offset++;
}
return [ a ];
}
// Case 2:
//
// Split position is inside `AttributeOperation#range`, at the same level, so the nodes to change are
// not going to make a flat range.
//
// Content with range-to-change and split position:
// Fo[zb^a]r
//
// After split:
// Fozb
ar
//
// Make two separate ranges containing all nodes to change:
// Fo[zb]
[a]r
//
if ( a.range.start.hasSameParentAs( b.splitPosition ) && a.range.containsPosition( b.splitPosition ) ) {
const secondPart = a.clone();
secondPart.range = new _range__WEBPACK_IMPORTED_MODULE_9__["default"](
b.moveTargetPosition.clone(),
a.range.end._getCombined( b.splitPosition, b.moveTargetPosition )
);
a.range.end = b.splitPosition.clone();
a.range.end.stickiness = 'toPrevious';
return [ a, secondPart ];
}
// The default case.
//
a.range = a.range._getTransformedBySplitOperation( b );
return [ a ];
} );
setTransformation( _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], _attributeoperation__WEBPACK_IMPORTED_MODULE_1__["default"], ( a, b ) => {
const result = [ a ];
// Case 1:
//
// The attribute operation range includes the position where nodes were inserted.
// There are two possible scenarios: the inserted nodes were text and they should receive attributes or
// the inserted nodes were elements and they should not receive attributes.
//
// This is a mirror scenario to the one described in `AttributeOperation` x `InsertOperation` transformation,
// although this case is a little less complicated. In this case we simply need to change attributes of the
// inserted nodes and that's it.
//
if ( a.shouldReceiveAttributes && a.position.hasSameParentAs( b.range.start ) && b.range.containsPosition( a.position ) ) {
const op = _getComplementaryAttributeOperations( a, b.key, b.newValue );
if ( op ) {
result.push( op );
}
}
// The default case is: do nothing.
// `AttributeOperation` does not change the model tree structure so `InsertOperation` does not need to be changed.
//
return result;
} );
setTransformation( _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], ( a, b, context ) => {
// Case 1:
//
// Two insert operations insert nodes at the same position. Since they are the same, it needs to be decided
// what will be the order of inserted nodes. However, there is no additional information to help in that
// decision. Also, when `b` will be transformed by `a`, the same order must be maintained.
//
// To achieve that, we will check if the operation is strong.
// If it is, it won't get transformed. If it is not, it will be moved.
//
if ( a.position.isEqual( b.position ) && context.aIsStrong ) {
return [ a ];
}
// The default case.
//
a.position = a.position._getTransformedByInsertOperation( b );
return [ a ];
} );
setTransformation( _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], ( a, b ) => {
// The default case.
//
a.position = a.position._getTransformedByMoveOperation( b );
return [ a ];
} );
setTransformation( _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], ( a, b ) => {
// The default case.
//
a.position = a.position._getTransformedBySplitOperation( b );
return [ a ];
} );
setTransformation( _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], ( a, b ) => {
a.position = a.position._getTransformedByMergeOperation( b );
return [ a ];
} );
// -----------------------
setTransformation( _markeroperation__WEBPACK_IMPORTED_MODULE_3__["default"], _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], ( a, b ) => {
if ( a.oldRange ) {
a.oldRange = a.oldRange._getTransformedByInsertOperation( b )[ 0 ];
}
if ( a.newRange ) {
a.newRange = a.newRange._getTransformedByInsertOperation( b )[ 0 ];
}
return [ a ];
} );
setTransformation( _markeroperation__WEBPACK_IMPORTED_MODULE_3__["default"], _markeroperation__WEBPACK_IMPORTED_MODULE_3__["default"], ( a, b, context ) => {
if ( a.name == b.name ) {
if ( context.aIsStrong ) {
a.oldRange = b.newRange ? b.newRange.clone() : null;
} else {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
}
return [ a ];
} );
setTransformation( _markeroperation__WEBPACK_IMPORTED_MODULE_3__["default"], _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], ( a, b ) => {
if ( a.oldRange ) {
a.oldRange = a.oldRange._getTransformedByMergeOperation( b );
}
if ( a.newRange ) {
a.newRange = a.newRange._getTransformedByMergeOperation( b );
}
return [ a ];
} );
setTransformation( _markeroperation__WEBPACK_IMPORTED_MODULE_3__["default"], _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], ( a, b, context ) => {
if ( a.oldRange ) {
a.oldRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromRanges( a.oldRange._getTransformedByMoveOperation( b ) );
}
if ( a.newRange ) {
if ( context.abRelation ) {
const aNewRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
if ( context.abRelation.side == 'left' && b.targetPosition.isEqual( a.newRange.start ) ) {
a.newRange.start.path = context.abRelation.path;
a.newRange.end = aNewRange.end;
return [ a ];
} else if ( context.abRelation.side == 'right' && b.targetPosition.isEqual( a.newRange.end ) ) {
a.newRange.start = aNewRange.start;
a.newRange.end.path = context.abRelation.path;
return [ a ];
}
}
a.newRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromRanges( a.newRange._getTransformedByMoveOperation( b ) );
}
return [ a ];
} );
setTransformation( _markeroperation__WEBPACK_IMPORTED_MODULE_3__["default"], _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], ( a, b, context ) => {
if ( a.oldRange ) {
a.oldRange = a.oldRange._getTransformedBySplitOperation( b );
}
if ( a.newRange ) {
if ( context.abRelation ) {
const aNewRange = a.newRange._getTransformedBySplitOperation( b );
if ( a.newRange.start.isEqual( b.splitPosition ) && context.abRelation.wasStartBeforeMergedElement ) {
a.newRange.start = _position__WEBPACK_IMPORTED_MODULE_10__["default"]._createAt( b.insertionPosition );
} else if ( a.newRange.start.isEqual( b.splitPosition ) && !context.abRelation.wasInLeftElement ) {
a.newRange.start = _position__WEBPACK_IMPORTED_MODULE_10__["default"]._createAt( b.moveTargetPosition );
}
if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasInRightElement ) {
a.newRange.end = _position__WEBPACK_IMPORTED_MODULE_10__["default"]._createAt( b.moveTargetPosition );
} else if ( a.newRange.end.isEqual( b.splitPosition ) && context.abRelation.wasEndBeforeMergedElement ) {
a.newRange.end = _position__WEBPACK_IMPORTED_MODULE_10__["default"]._createAt( b.insertionPosition );
} else {
a.newRange.end = aNewRange.end;
}
return [ a ];
}
a.newRange = a.newRange._getTransformedBySplitOperation( b );
}
return [ a ];
} );
// -----------------------
setTransformation( _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], ( a, b ) => {
if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
a.howMany += b.howMany;
}
a.sourcePosition = a.sourcePosition._getTransformedByInsertOperation( b );
a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
return [ a ];
} );
setTransformation( _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], ( a, b, context ) => {
// Case 1:
//
// Same merge operations.
//
// Both operations have same source and target positions. So the element already got merged and there is
// theoretically nothing to do.
//
if ( a.sourcePosition.isEqual( b.sourcePosition ) && a.targetPosition.isEqual( b.targetPosition ) ) {
// There are two ways that we can provide a do-nothing operation.
//
// First is simply a NoOperation instance. We will use it if `b` operation was not undone.
//
// Second is a merge operation that has the source operation in the merged element - in the graveyard -
// same target position and `howMany` equal to `0`. So it is basically merging an empty element from graveyard
// which is almost the same as NoOperation.
//
// This way the merge operation can be later transformed by split operation
// to provide correct undo. This will be used if `b` operation was undone (only then it is correct).
//
if ( !context.bWasUndone ) {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
} else {
const path = b.graveyardPosition.path.slice();
path.push( 0 );
a.sourcePosition = new _position__WEBPACK_IMPORTED_MODULE_10__["default"]( b.graveyardPosition.root, path );
a.howMany = 0;
return [ a ];
}
}
// Case 2:
//
// Same merge source position but different target position.
//
// This can happen during collaboration. For example, if one client merged a paragraph to the previous paragraph
// and the other person removed that paragraph and merged the same paragraph to something before:
//
// Client A:
// Foo
Bar
[]Xyz
// Foo
BarXyz
//
// Client B:
// Foo
[Bar
]Xyz
// Foo
[]Xyz
// FooXyz
//
// In this case we need to decide where finally "Xyz" will land:
//
// FooXyz
graveyard: Bar
// Foo
graveyard: BarXyz
//
// Let's move it in a way so that a merge operation that does not target to graveyard is more important so that
// nodes does not end up in the graveyard. It makes sense. Both for Client A and for Client B "Xyz" finally did not
// end up in the graveyard (see above).
//
// If neither or both operations point to graveyard, then let `aIsStrong` decide.
//
if (
a.sourcePosition.isEqual( b.sourcePosition ) && !a.targetPosition.isEqual( b.targetPosition ) &&
!context.bWasUndone && context.abRelation != 'splitAtSource'
) {
const aToGraveyard = a.targetPosition.root.rootName == '$graveyard';
const bToGraveyard = b.targetPosition.root.rootName == '$graveyard';
// If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
const aIsWeak = aToGraveyard && !bToGraveyard;
// If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
const bIsWeak = bToGraveyard && !aToGraveyard;
// Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
const forceMove = bIsWeak || ( !aIsWeak && context.aIsStrong );
if ( forceMove ) {
const sourcePosition = b.targetPosition._getTransformedByMergeOperation( b );
const targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
return [ new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( sourcePosition, a.howMany, targetPosition, 0 ) ];
} else {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
}
// The default case.
//
if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
a.howMany += b.howMany;
}
a.sourcePosition = a.sourcePosition._getTransformedByMergeOperation( b );
a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
// Handle positions in graveyard.
// If graveyard positions are same and `a` operation is strong - do not transform.
if ( !a.graveyardPosition.isEqual( b.graveyardPosition ) || !context.aIsStrong ) {
a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation( b );
}
return [ a ];
} );
setTransformation( _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], ( a, b, context ) => {
// Case 1:
//
// The element to merge got removed.
//
// Merge operation does support merging elements which are not siblings. So it would not be a problem
// from technical point of view. However, if the element was removed, the intention of the user deleting it
// was to have it all deleted, together with its children. From user experience point of view, moving back the
// removed nodes might be unexpected. This means that in this scenario we will block the merging.
//
// The exception of this rule would be if the remove operation was later undone.
//
const removedRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( b.sourcePosition, b.howMany );
if ( b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
if ( a.deletionPosition.hasSameParentAs( b.sourcePosition ) && removedRange.containsPosition( a.sourcePosition ) ) {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
}
// The default case.
//
if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
a.howMany += b.howMany;
}
if ( a.sourcePosition.hasSameParentAs( b.sourcePosition ) ) {
a.howMany -= b.howMany;
}
a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation( b );
a.targetPosition = a.targetPosition._getTransformedByMoveOperation( b );
// `MergeOperation` graveyard position is like `MoveOperation` target position. It is a position where element(s) will
// be moved. Like in other similar cases, we need to consider the scenario when those positions are same.
// Here, we will treat `MergeOperation` like it is always strong (see `InsertOperation` x `InsertOperation` for comparison).
// This means that we won't transform graveyard position if it is equal to move operation target position.
if ( !a.graveyardPosition.isEqual( b.targetPosition ) ) {
a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
}
return [ a ];
} );
setTransformation( _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], ( a, b, context ) => {
if ( b.graveyardPosition ) {
// If `b` operation defines graveyard position, a node from graveyard will be moved. This means that we need to
// transform `a.graveyardPosition` accordingly.
a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
// This is a scenario foreseen in `MergeOperation` x `MergeOperation`, with two identical merge operations.
//
// So, there was `MergeOperation` x `MergeOperation` transformation earlier. Now, `a` is a merge operation which
// source position is in graveyard. Interestingly, split operation wants to use the node to be merged by `a`. This
// means that `b` is undoing that merge operation from earlier, which caused `a` to be in graveyard.
//
// If that's the case, at this point, we will only "fix" `a.howMany`. It was earlier set to `0` in
// `MergeOperation` x `MergeOperation` transformation. Later transformations in this function will change other
// properties.
//
if ( a.deletionPosition.isEqual( b.graveyardPosition ) ) {
a.howMany = b.howMany;
}
}
// Case 1:
//
// Merge operation moves nodes to the place where split happens.
// This is a classic situation when there are two paragraphs, and there is a split (enter) after the first
// paragraph and there is a merge (delete) at the beginning of the second paragraph:
//
// Foo{}
[]Bar
.
//
// Split is after `Foo`, while merge is from `Bar` to the end of `Foo`.
//
// State after split:
// Foo
Bar
//
// Now, `Bar` should be merged to the new paragraph:
// Foo
Bar
//
// Instead of merging it to the original paragraph:
// FooBar
//
// This means that `targetPosition` needs to be transformed. This is the default case though.
// For example, if the split would be after `F`, `targetPosition` should also be transformed.
//
// There are three exceptions, though, when we want to keep `targetPosition` as it was.
//
// First exception is when the merge target position is inside an element (not at the end, as usual). This
// happens when the merge operation earlier was transformed by "the same" merge operation. If merge operation
// targets inside the element we want to keep the original target position (and not transform it) because
// we have additional context telling us that we want to merge to the original element. We can check if the
// merge operation points inside element by checking what is `SplitOperation#howMany`. Since merge target position
// is same as split position, if `howMany` is non-zero, it means that the merge target position is inside an element.
//
// Second exception is when the element to merge is in the graveyard and split operation uses it. In that case
// if target position would be transformed, the merge operation would target at the source position:
//
// root: Foo
graveyard:
//
// SplitOperation: root [ 0, 3 ] using graveyard [ 0 ] (howMany = 0)
// MergeOperation: graveyard [ 0, 0 ] -> root [ 0, 3 ] (howMany = 0)
//
// Since split operation moves the graveyard node back to the root, the merge operation source position changes.
// We would like to merge from the empty to the "Foo"
:
//
// root:
Foo
graveyard:
//
// MergeOperation#sourcePosition = root [ 1, 0 ]
//
// If `targetPosition` is transformed, it would become root [ 1, 0 ] as well. It has to be kept as it was.
//
// Third exception is connected with relations. If this happens during undo and we have explicit information
// that target position has not been affected by the operation which is undone by this split then this split should
// not move the target position either.
//
if ( a.targetPosition.isEqual( b.splitPosition ) ) {
const mergeInside = b.howMany != 0;
const mergeSplittingElement = b.graveyardPosition && a.deletionPosition.isEqual( b.graveyardPosition );
if ( mergeInside || mergeSplittingElement || context.abRelation == 'mergeTargetNotMoved' ) {
a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
return [ a ];
}
}
// Case 2:
//
// Merge source is at the same position as split position. This sometimes happen, mostly during undo.
// The decision here is mostly to choose whether merge source position should stay where it is (so it will be at the end of the
// split element) or should be move to the beginning of the new element.
//
if ( a.sourcePosition.isEqual( b.splitPosition ) ) {
// Use context to check if `SplitOperation` is not undoing a merge operation, that didn't change the `a` operation.
// This scenario happens the undone merge operation moved nodes at the source position of `a` operation.
// In that case `a` operation source position should stay where it is.
if ( context.abRelation == 'mergeSourceNotMoved' ) {
a.howMany = 0;
a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
return [ a ];
}
// This merge operation might have been earlier transformed by a merge operation which both merged the same element.
// See that case in `MergeOperation` x `MergeOperation` transformation. In that scenario, if the merge operation has been undone,
// the special case is not applied.
//
// Now, the merge operation is transformed by the split which has undone that previous merge operation.
// So now we are fixing situation which was skipped in `MergeOperation` x `MergeOperation` case.
//
if ( context.abRelation == 'mergeSameElement' || a.sourcePosition.offset > 0 ) {
a.sourcePosition = b.moveTargetPosition.clone();
a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
return [ a ];
}
}
// The default case.
//
if ( a.sourcePosition.hasSameParentAs( b.splitPosition ) ) {
a.howMany = b.splitPosition.offset;
}
a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
return [ a ];
} );
// -----------------------
setTransformation( _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], ( a, b ) => {
const moveRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( a.sourcePosition, a.howMany );
const transformed = moveRange._getTransformedByInsertOperation( b, false )[ 0 ];
a.sourcePosition = transformed.start;
a.howMany = transformed.end.offset - transformed.start.offset;
// See `InsertOperation` x `MoveOperation` transformation for details on this case.
//
// In summary, both operations point to the same place, so the order of nodes needs to be decided.
// `MoveOperation` is considered weaker, so it is always transformed, unless there was a certain relation
// between operations.
//
if ( !a.targetPosition.isEqual( b.position ) ) {
a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
}
return [ a ];
} );
setTransformation( _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], ( a, b, context ) => {
//
// Setting and evaluating some variables that will be used in special cases and default algorithm.
//
// Create ranges from `MoveOperations` properties.
const rangeA = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( a.sourcePosition, a.howMany );
const rangeB = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( b.sourcePosition, b.howMany );
// Assign `context.aIsStrong` to a different variable, because the value may change during execution of
// this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
let aIsStrong = context.aIsStrong;
// This will be used to decide the order of nodes if both operations target at the same position.
// By default, use strong/weak operation mechanism.
let insertBefore = !context.aIsStrong;
// If the relation is set, then use it to decide nodes order.
if ( context.abRelation == 'insertBefore' || context.baRelation == 'insertAfter' ) {
insertBefore = true;
} else if ( context.abRelation == 'insertAfter' || context.baRelation == 'insertBefore' ) {
insertBefore = false;
}
// `a.targetPosition` could be affected by the `b` operation. We will transform it.
let newTargetPosition;
if ( a.targetPosition.isEqual( b.targetPosition ) && insertBefore ) {
newTargetPosition = a.targetPosition._getTransformedByDeletion(
b.sourcePosition,
b.howMany
);
} else {
newTargetPosition = a.targetPosition._getTransformedByMove(
b.sourcePosition,
b.targetPosition,
b.howMany
);
}
//
// Special case #1 + mirror.
//
// Special case when both move operations' target positions are inside nodes that are
// being moved by the other move operation. So in other words, we move ranges into inside of each other.
// This case can't be solved reasonably (on the other hand, it should not happen often).
if ( _moveTargetIntoMovedRange( a, b ) && _moveTargetIntoMovedRange( b, a ) ) {
// Instead of transforming operation, we return a reverse of the operation that we transform by.
// So when the results of this "transformation" will be applied, `b` MoveOperation will get reversed.
return [ b.getReversed() ];
}
//
// End of special case #1.
//
//
// Special case #2.
//
// Check if `b` operation targets inside `rangeA`.
const bTargetsToA = rangeA.containsPosition( b.targetPosition );
// If `b` targets to `rangeA` and `rangeA` contains `rangeB`, `b` operation has no influence on `a` operation.
// You might say that operation `b` is captured inside operation `a`.
if ( bTargetsToA && rangeA.containsRange( rangeB, true ) ) {
// There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
// we need to transform `a` operation anyway.
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
return _makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition );
}
//
// Special case #2 mirror.
//
const aTargetsToB = rangeB.containsPosition( a.targetPosition );
if ( aTargetsToB && rangeB.containsRange( rangeA, true ) ) {
// `a` operation is "moved together" with `b` operation.
// Here, just move `rangeA` "inside" `rangeB`.
rangeA.start = rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
rangeA.end = rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
return _makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition );
}
//
// End of special case #2.
//
//
// Special case #3 + mirror.
//
// `rangeA` has a node which is an ancestor of `rangeB`. In other words, `rangeB` is inside `rangeA`
// but not on the same tree level. In such case ranges have common part but we have to treat it
// differently, because in such case those ranges are not really conflicting and should be treated like
// two separate ranges. Also we have to discard two difference parts.
const aCompB = Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_11__["default"])( a.sourcePosition.getParentPath(), b.sourcePosition.getParentPath() );
if ( aCompB == 'prefix' || aCompB == 'extension' ) {
// Transform `rangeA` by `b` operation and make operation out of it, and that's all.
// Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
// like a one difference part.
rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany );
return _makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition );
}
//
// End of special case #3.
//
//
// Default case - ranges are on the same level or are not connected with each other.
//
// Modifier for default case.
// Modifies `aIsStrong` flag in certain conditions.
//
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
// to provide more expected results.
if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove ) {
aIsStrong = true;
} else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
aIsStrong = false;
}
// Handle operation's source ranges - check how `rangeA` is affected by `b` operation.
// This will aggregate transformed ranges.
const ranges = [];
// Get the "difference part" of `a` operation source range.
// This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
const difference = rangeA.getDifference( rangeB );
for ( const range of difference ) {
// Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
range.start = range.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
range.end = range.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
// If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
const shouldSpread = Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_11__["default"])( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same';
const newRanges = range._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, shouldSpread );
ranges.push( ...newRanges );
}
// Then, we have to manage the "common part" of both move ranges.
const common = rangeA.getIntersection( rangeB );
if ( common !== null && aIsStrong ) {
// Calculate the new position of that part of original range.
common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
// Take care of proper range order.
//
// Put `common` at appropriate place. Keep in mind that we are interested in original order.
// Basically there are only three cases: there is zero, one or two difference ranges.
//
// If there is zero difference ranges, just push `common` in the array.
if ( ranges.length === 0 ) {
ranges.push( common );
}
// If there is one difference range, we need to check whether common part was before it or after it.
else if ( ranges.length == 1 ) {
if ( rangeB.start.isBefore( rangeA.start ) || rangeB.start.isEqual( rangeA.start ) ) {
ranges.unshift( common );
} else {
ranges.push( common );
}
}
// If there are more ranges (which means two), put common part between them. This is the only scenario
// where there could be two difference ranges so we don't have to make any comparisons.
else {
ranges.splice( 1, 0, common );
}
}
if ( ranges.length === 0 ) {
// If there are no "source ranges", nothing should be changed.
// Note that this can happen only if `aIsStrong == false` and `rangeA.isEqual( rangeB )`.
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( a.baseVersion ) ];
}
return _makeMoveOperationsFromRanges( ranges, newTargetPosition );
} );
setTransformation( _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], ( a, b, context ) => {
let newTargetPosition = a.targetPosition.clone();
// Do not transform if target position is same as split insertion position and this split comes from undo.
// This should be done on relations but it is too much work for now as it would require relations working in collaboration.
// We need to make a decision how we will resolve such conflict and this is less harmful way.
if ( !a.targetPosition.isEqual( b.insertionPosition ) || !b.graveyardPosition || context.abRelation == 'moveTargetAfter' ) {
newTargetPosition = a.targetPosition._getTransformedBySplitOperation( b );
}
// Case 1:
//
// Last element in the moved range got split.
//
// In this case the default range transformation will not work correctly as the element created by
// split operation would be outside the range. The range to move needs to be fixed manually.
//
const moveRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( a.sourcePosition, a.howMany );
if ( moveRange.end.isEqual( b.insertionPosition ) ) {
// Do it only if this is a "natural" split, not a one that comes from undo.
// If this is undo split, only `targetPosition` needs to be changed (if the move is a remove).
if ( !b.graveyardPosition ) {
a.howMany++;
}
a.targetPosition = newTargetPosition;
return [ a ];
}
// Case 2:
//
// Split happened between the moved nodes. In this case two ranges to move need to be generated.
//
// Characters `ozba` are moved to the end of paragraph `Xyz` but split happened.
// F[oz|ba]r
Xyz
//
// After split:
// F[oz
ba]r
Xyz
//
// Correct ranges:
// F[oz]
[ba]r
Xyz
//
// After move:
// F
r
Xyzozba
//
if ( moveRange.start.hasSameParentAs( b.splitPosition ) && moveRange.containsPosition( b.splitPosition ) ) {
let rightRange = new _range__WEBPACK_IMPORTED_MODULE_9__["default"]( b.splitPosition, moveRange.end );
rightRange = rightRange._getTransformedBySplitOperation( b );
const ranges = [
new _range__WEBPACK_IMPORTED_MODULE_9__["default"]( moveRange.start, b.splitPosition ),
rightRange
];
return _makeMoveOperationsFromRanges( ranges, newTargetPosition );
}
// Case 3:
//
// Move operation targets at the split position. We need to decide if the nodes should be inserted
// at the end of the split element or at the beginning of the new element.
//
if ( a.targetPosition.isEqual( b.splitPosition ) && context.abRelation == 'insertAtSource' ) {
newTargetPosition = b.moveTargetPosition;
}
// Case 4:
//
// Move operation targets just after the split element. We need to decide if the nodes should be inserted
// between two parts of split element, or after the new element.
//
// Split at `|`, while move operation moves `Xyz
` and targets at `^`:
// Foo|bar
^baz
// Foo
^bar
baz
or Foo
bar
^baz
?
//
// If there is no contextual information between operations (for example, they come from collaborative
// editing), we don't want to put some unrelated content (move) between parts of related content (split parts).
// However, if the split is from undo, in the past, the moved content might be targeting between the
// split parts, meaning that was exactly user's intention:
//
// Foo
^bar
<--- original situation, in "past".
// Foobar
^ <--- after merge target position is transformed.
// Foo|bar
^ <--- then the merge is undone, and split happens, which leads us to current situation.
//
// In this case it is pretty clear that the intention was to put new paragraph between those nodes,
// so we need to transform accordingly. We can detect this scenario thanks to relations.
//
if ( a.targetPosition.isEqual( b.insertionPosition ) && context.abRelation == 'insertBetween' ) {
newTargetPosition = a.targetPosition;
}
// The default case.
//
const transformed = moveRange._getTransformedBySplitOperation( b );
const ranges = [ transformed ];
// Case 5:
//
// Moved range contains graveyard element used by split operation. Add extra move operation to the result.
//
if ( b.graveyardPosition ) {
const movesGraveyardElement = moveRange.start.isEqual( b.graveyardPosition ) || moveRange.containsPosition( b.graveyardPosition );
if ( a.howMany > 1 && movesGraveyardElement && !context.aWasUndone ) {
ranges.push( _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( b.insertionPosition, 1 ) );
}
}
return _makeMoveOperationsFromRanges( ranges, newTargetPosition );
} );
setTransformation( _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], ( a, b, context ) => {
const movedRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( a.sourcePosition, a.howMany );
if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
if ( a.type == 'remove' && !context.forceWeakRemove ) {
// Case 1:
//
// The element to remove got merged.
//
// Merge operation does support merging elements which are not siblings. So it would not be a problem
// from technical point of view. However, if the element was removed, the intention of the user
// deleting it was to have it all deleted. From user experience point of view, moving back the
// removed nodes might be unexpected. This means that in this scenario we will reverse merging and remove the element.
//
if ( !context.aWasUndone ) {
const results = [];
let gyMoveSource = b.graveyardPosition.clone();
let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation( b );
if ( a.howMany > 1 ) {
results.push( new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( a.sourcePosition, a.howMany - 1, a.targetPosition, 0 ) );
gyMoveSource = gyMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
}
const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
const gyMove = new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( gyMoveSource, 1, gyMoveTarget, 0 );
const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
splitNodesMoveTargetPath.push( 0 );
const splitNodesMoveTarget = new _position__WEBPACK_IMPORTED_MODULE_10__["default"]( gyMove.targetPosition.root, splitNodesMoveTargetPath );
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( gyMoveSource, gyMoveTarget, 1 );
const splitNodesMove = new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );
results.push( gyMove );
results.push( splitNodesMove );
return results;
}
} else {
// Case 2:
//
// The element to move got merged and it was the only element to move.
// In this case just don't do anything, leave the node in the graveyard. Without special case
// it would be a move operation that moves 0 nodes, so maybe it is better just to return no-op.
//
if ( a.howMany == 1 ) {
if ( !context.bWasUndone ) {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
} else {
a.sourcePosition = b.graveyardPosition.clone();
a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
return [ a ];
}
}
}
}
// The default case.
//
const moveRange = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( a.sourcePosition, a.howMany );
const transformed = moveRange._getTransformedByMergeOperation( b );
a.sourcePosition = transformed.start;
a.howMany = transformed.end.offset - transformed.start.offset;
a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
return [ a ];
} );
// -----------------------
setTransformation( _renameoperation__WEBPACK_IMPORTED_MODULE_2__["default"], _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], ( a, b ) => {
a.position = a.position._getTransformedByInsertOperation( b );
return [ a ];
} );
setTransformation( _renameoperation__WEBPACK_IMPORTED_MODULE_2__["default"], _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], ( a, b ) => {
// Case 1:
//
// Element to rename got merged, so it was moved to `b.graveyardPosition`.
//
if ( a.position.isEqual( b.deletionPosition ) ) {
a.position = b.graveyardPosition.clone();
a.position.stickiness = 'toNext';
return [ a ];
}
a.position = a.position._getTransformedByMergeOperation( b );
return [ a ];
} );
setTransformation( _renameoperation__WEBPACK_IMPORTED_MODULE_2__["default"], _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], ( a, b ) => {
a.position = a.position._getTransformedByMoveOperation( b );
return [ a ];
} );
setTransformation( _renameoperation__WEBPACK_IMPORTED_MODULE_2__["default"], _renameoperation__WEBPACK_IMPORTED_MODULE_2__["default"], ( a, b, context ) => {
if ( a.position.isEqual( b.position ) ) {
if ( context.aIsStrong ) {
a.oldName = b.newName;
} else {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
}
return [ a ];
} );
setTransformation( _renameoperation__WEBPACK_IMPORTED_MODULE_2__["default"], _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], ( a, b ) => {
// Case 1:
//
// The element to rename has been split. In this case, the new element should be also renamed.
//
// User decides to change the paragraph to a list item:
// Foobar
//
// However, in meantime, split happens:
// Foo bar
//
// As a result, rename both elements:
// Foo bar
//
const renamePath = a.position.path;
const splitPath = b.splitPosition.getParentPath();
if ( Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_11__["default"])( renamePath, splitPath ) == 'same' && !b.graveyardPosition ) {
const extraRename = new _renameoperation__WEBPACK_IMPORTED_MODULE_2__["default"]( a.position.getShiftedBy( 1 ), a.oldName, a.newName, 0 );
return [ a, extraRename ];
}
// The default case.
//
a.position = a.position._getTransformedBySplitOperation( b );
return [ a ];
} );
// -----------------------
setTransformation( _rootattributeoperation__WEBPACK_IMPORTED_MODULE_5__["default"], _rootattributeoperation__WEBPACK_IMPORTED_MODULE_5__["default"], ( a, b, context ) => {
if ( a.root === b.root && a.key === b.key ) {
if ( !context.aIsStrong || a.newValue === b.newValue ) {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
} else {
a.oldValue = b.newValue;
}
}
return [ a ];
} );
// -----------------------
setTransformation( _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], _insertoperation__WEBPACK_IMPORTED_MODULE_0__["default"], ( a, b ) => {
// The default case.
//
if ( a.splitPosition.hasSameParentAs( b.position ) && a.splitPosition.offset < b.position.offset ) {
a.howMany += b.howMany;
}
a.splitPosition = a.splitPosition._getTransformedByInsertOperation( b );
a.insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( a.splitPosition );
return [ a ];
} );
setTransformation( _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], _mergeoperation__WEBPACK_IMPORTED_MODULE_6__["default"], ( a, b, context ) => {
// Case 1:
//
// Split element got merged. If two different elements were merged, clients will have different content.
//
// Example. Merge at `{}`, split at `[]`:
// Foo {}B[]ar
//
// On merge side it will look like this:
// FooB[]ar
// FooB ar
//
// On split side it will look like this:
// Foo {}B ar
// FooB ar
//
// Clearly, the second element is different for both clients.
//
// We could use the removed merge element from graveyard as a split element but then clients would have a different
// model state (in graveyard), because the split side client would still have an element in graveyard (removed by merge).
//
// To overcome this, in `SplitOperation` x `MergeOperation` transformation we will add additional `SplitOperation`
// in the graveyard, which will actually clone the merged-and-deleted element. Then, that cloned element will be
// used for splitting. Example below.
//
// Original state:
// Foo {}B[]ar
//
// Merge side client:
//
// After merge:
// FooB[]ar graveyard:
//
// Extra split:
// FooB[]ar graveyard:
//
// Use the "cloned" element from graveyard:
// FooB ar graveyard:
//
// Split side client:
//
// After split:
// Foo {}B ar
//
// After merge:
// FooB ar graveyard:
//
// This special case scenario only applies if the original split operation clones the split element.
// If the original split operation has `graveyardPosition` set, it all doesn't have sense because split operation
// knows exactly which element it should use. So there would be no original problem with different contents.
//
// Additionally, the special case applies only if the merge wasn't already undone.
//
if ( !a.graveyardPosition && !context.bWasUndone && a.splitPosition.hasSameParentAs( b.sourcePosition ) ) {
const splitPath = b.graveyardPosition.path.slice();
splitPath.push( 0 );
const splitPosition = new _position__WEBPACK_IMPORTED_MODULE_10__["default"]( b.graveyardPosition.root, splitPath );
const insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( new _position__WEBPACK_IMPORTED_MODULE_10__["default"]( b.graveyardPosition.root, splitPath ) );
const additionalSplit = new _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"]( splitPosition, 0, null, 0 );
additionalSplit.insertionPosition = insertionPosition;
a.splitPosition = a.splitPosition._getTransformedByMergeOperation( b );
a.insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( a.splitPosition );
a.graveyardPosition = additionalSplit.insertionPosition.clone();
a.graveyardPosition.stickiness = 'toNext';
return [ additionalSplit, a ];
}
// The default case.
//
if ( a.splitPosition.hasSameParentAs( b.deletionPosition ) && !a.splitPosition.isAfter( b.deletionPosition ) ) {
a.howMany--;
}
if ( a.splitPosition.hasSameParentAs( b.targetPosition ) ) {
a.howMany += b.howMany;
}
a.splitPosition = a.splitPosition._getTransformedByMergeOperation( b );
a.insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( a.splitPosition );
if ( a.graveyardPosition ) {
a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation( b );
}
return [ a ];
} );
setTransformation( _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"], ( a, b, context ) => {
const rangeToMove = _range__WEBPACK_IMPORTED_MODULE_9__["default"]._createFromPositionAndShift( b.sourcePosition, b.howMany );
if ( a.graveyardPosition ) {
// Case 1:
//
// Split operation graveyard node was moved. In this case move operation is stronger. Since graveyard element
// is already moved to the correct position, we need to only move the nodes after the split position.
// This will be done by `MoveOperation` instead of `SplitOperation`.
//
const gyElementMoved = rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition );
if ( !context.bWasUndone && gyElementMoved ) {
const sourcePosition = a.splitPosition._getTransformedByMoveOperation( b );
const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
const newTargetPath = newParentPosition.path.slice();
newTargetPath.push( 0 );
const newTargetPosition = new _position__WEBPACK_IMPORTED_MODULE_10__["default"]( newParentPosition.root, newTargetPath );
const moveOp = new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( sourcePosition, a.howMany, newTargetPosition, 0 );
return [ moveOp ];
}
a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
}
// Case 2:
//
// If the split position is inside the moved range, we need to shift the split position to a proper place.
// The position cannot be moved together with moved range because that would result in splitting of an incorrect element.
//
// Characters `bc` should be moved to the second paragraph while split position is between them:
// A[b|c]d Xyz
//
// After move, new split position is incorrect:
// Ad Xb|cyz
//
// Correct split position:
// A|d Xbcyz
//
// After split:
// A d Xbcyz
//
if ( a.splitPosition.hasSameParentAs( b.sourcePosition ) && rangeToMove.containsPosition( a.splitPosition ) ) {
const howManyRemoved = b.howMany - ( a.splitPosition.offset - b.sourcePosition.offset );
a.howMany -= howManyRemoved;
if ( a.splitPosition.hasSameParentAs( b.targetPosition ) && a.splitPosition.offset < b.targetPosition.offset ) {
a.howMany += b.howMany;
}
a.splitPosition = b.sourcePosition.clone();
a.insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( a.splitPosition );
return [ a ];
}
// Case 3:
//
// Split is at a position where nodes were moved.
//
// This is a scenario described in `MoveOperation` x `SplitOperation` transformation but from the
// "split operation point of view".
//
const splitAtTarget = a.splitPosition.isEqual( b.targetPosition );
if ( splitAtTarget && ( context.baRelation == 'insertAtSource' || context.abRelation == 'splitBefore' ) ) {
a.howMany += b.howMany;
a.splitPosition = a.splitPosition._getTransformedByDeletion( b.sourcePosition, b.howMany );
a.insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( a.splitPosition );
return [ a ];
}
// The default case.
// Don't change `howMany` if move operation does not really move anything.
//
if ( !b.sourcePosition.isEqual( b.targetPosition ) ) {
if ( a.splitPosition.hasSameParentAs( b.sourcePosition ) && a.splitPosition.offset <= b.sourcePosition.offset ) {
a.howMany -= b.howMany;
}
if ( a.splitPosition.hasSameParentAs( b.targetPosition ) && a.splitPosition.offset < b.targetPosition.offset ) {
a.howMany += b.howMany;
}
}
// Change position stickiness to force a correct transformation.
a.splitPosition.stickiness = 'toNone';
a.splitPosition = a.splitPosition._getTransformedByMoveOperation( b );
a.splitPosition.stickiness = 'toNext';
if ( a.graveyardPosition ) {
a.insertionPosition = a.insertionPosition._getTransformedByMoveOperation( b );
} else {
a.insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( a.splitPosition );
}
return [ a ];
} );
setTransformation( _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"], ( a, b, context ) => {
// Case 1:
//
// Split at the same position.
//
// If there already was a split at the same position as in `a` operation, it means that the intention
// conveyed by `a` operation has already been fulfilled and `a` should not do anything (to avoid double split).
//
// However, there is a difference if these are new splits or splits created by undo. These have different
// intentions. Also splits moving back different elements from graveyard have different intentions. They
// are just different operations.
//
// So we cancel split operation only if it was really identical.
//
// Also, there is additional case, where split operations aren't identical and should not be cancelled, however the
// default transformation is incorrect too.
//
if ( a.splitPosition.isEqual( b.splitPosition ) ) {
if ( !a.graveyardPosition && !b.graveyardPosition ) {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
// Use context to know that the `a.splitPosition` should stay where it is.
// This happens during undo when first a merge operation moved nodes to `a.splitPosition` and now `b` operation undoes that merge.
if ( context.abRelation == 'splitBefore' ) {
// Since split is at the same position, there are no nodes left to split.
a.howMany = 0;
// Note: there was `if ( a.graveyardPosition )` here but it was uncovered in tests and I couldn't find any scenarios for now.
// That would have to be a `SplitOperation` that didn't come from undo but is transformed by operations that were undone.
// It could happen if `context` is enabled in collaboration.
a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation( b );
return [ a ];
}
}
// Case 2:
//
// Same node is using to split different elements. This happens in undo when previously same element was merged to
// two different elements. This is described in `MergeOperation` x `MergeOperation` transformation.
//
// In this case we will follow the same logic. We will assume that `insertionPosition` is same for both
// split operations. This might not always be true but in the real cases that were experienced it was. After all,
// if these splits are reverses of merge operations that were merging the same element, then the `insertionPosition`
// should be same for both of those splits.
//
// Again, we will decide which operation is stronger by checking if split happens in graveyard or in non-graveyard root.
//
if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
const aInGraveyard = a.splitPosition.root.rootName == '$graveyard';
const bInGraveyard = b.splitPosition.root.rootName == '$graveyard';
// If `aIsWeak` it means that `a` points to graveyard while `b` doesn't. Don't move nodes then.
const aIsWeak = aInGraveyard && !bInGraveyard;
// If `bIsWeak` it means that `b` points to graveyard while `a` doesn't. Force moving nodes then.
const bIsWeak = bInGraveyard && !aInGraveyard;
// Force move if `b` is weak or neither operation is weak but `a` is stronger through `context.aIsStrong`.
const forceMove = bIsWeak || ( !aIsWeak && context.aIsStrong );
if ( forceMove ) {
const result = [];
// First we need to move any nodes split by `b` back to where they were.
// Do it only if `b` actually moved something.
if ( b.howMany ) {
result.push( new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( b.moveTargetPosition, b.howMany, b.splitPosition, 0 ) );
}
// Then we need to move nodes from `a` split position to their new element.
// Do it only if `a` actually should move something.
if ( a.howMany ) {
result.push( new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( a.splitPosition, a.howMany, a.moveTargetPosition, 0 ) );
}
return result;
} else {
return [ new _nooperation__WEBPACK_IMPORTED_MODULE_8__["default"]( 0 ) ];
}
}
if ( a.graveyardPosition ) {
a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation( b );
}
// Case 3:
//
// Position where operation `b` inserted a new node after split is the same as the operation `a` split position.
// As in similar cases, there is ambiguity if the split should be before the new node (created by `b`) or after.
//
if ( a.splitPosition.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
a.howMany++;
return [ a ];
}
// Case 4:
//
// This is a mirror to the case 2. above.
//
if ( b.splitPosition.isEqual( a.insertionPosition ) && context.baRelation == 'splitBefore' ) {
const newPositionPath = b.insertionPosition.path.slice();
newPositionPath.push( 0 );
const newPosition = new _position__WEBPACK_IMPORTED_MODULE_10__["default"]( b.insertionPosition.root, newPositionPath );
const moveOp = new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"]( a.insertionPosition, 1, newPosition, 0 );
return [ a, moveOp ];
}
// The default case.
//
if ( a.splitPosition.hasSameParentAs( b.splitPosition ) && a.splitPosition.offset < b.splitPosition.offset ) {
a.howMany -= b.howMany;
}
a.splitPosition = a.splitPosition._getTransformedBySplitOperation( b );
a.insertionPosition = _splitoperation__WEBPACK_IMPORTED_MODULE_7__["default"].getInsertionPosition( a.splitPosition );
return [ a ];
} );
// Checks whether `MoveOperation` `targetPosition` is inside a node from the moved range of the other `MoveOperation`.
//
// @private
// @param {module:engine/model/operation/moveoperation~MoveOperation} a
// @param {module:engine/model/operation/moveoperation~MoveOperation} b
// @returns {Boolean}
function _moveTargetIntoMovedRange( a, b ) {
return a.targetPosition._getTransformedByDeletion( b.sourcePosition, b.howMany ) === null;
}
// Helper function for `MoveOperation` x `MoveOperation` transformation. Converts given ranges and target position to
// move operations and returns them.
//
// Ranges and target position will be transformed on-the-fly when generating operations.
//
// Given `ranges` should be in the order of how they were in the original transformed operation.
//
// Given `targetPosition` is the target position of the first range from `ranges`.
//
// @private
// @param {Array.} ranges
// @param {module:engine/model/position~Position} targetPosition
// @returns {Array.}
function _makeMoveOperationsFromRanges( ranges, targetPosition ) {
// At this moment we have some ranges and a target position, to which those ranges should be moved.
// Order in `ranges` array is the go-to order of after transformation.
//
// We are almost done. We have `ranges` and `targetPosition` to make operations from.
// Unfortunately, those operations may affect each other. Precisely, first operation after move
// may affect source range and target position of second and third operation. Same with second
// operation affecting third.
//
// We need to fix those source ranges and target positions once again, before converting `ranges` to operations.
const operations = [];
// Keep in mind that nothing will be transformed if there is just one range in `ranges`.
for ( let i = 0; i < ranges.length; i++ ) {
// Create new operation out of a range and target position.
const range = ranges[ i ];
const op = new _moveoperation__WEBPACK_IMPORTED_MODULE_4__["default"](
range.start,
range.end.offset - range.start.offset,
targetPosition,
0
);
operations.push( op );
// Transform other ranges by the generated operation.
for ( let j = i + 1; j < ranges.length; j++ ) {
// All ranges in `ranges` array should be:
//
// * non-intersecting (these are part of original operation source range), and
// * `targetPosition` does not target into them (opposite would mean that transformed operation targets "inside itself").
//
// This means that the transformation will be "clean" and always return one result.
ranges[ j ] = ranges[ j ]._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany )[ 0 ];
}
targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany );
}
return operations;
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js":
/*!******************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/operation/utils.js ***!
\******************************************************************************/
/*! exports provided: _insert, _remove, _move, _setAttribute, _normalizeNodes */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_insert", function() { return _insert; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_remove", function() { return _remove; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_move", function() { return _move; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_setAttribute", function() { return _setAttribute; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_normalizeNodes", function() { return _normalizeNodes; });
/* harmony import */ var _node__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../node */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js");
/* harmony import */ var _text__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../text */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js");
/* harmony import */ var _textproxy__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../textproxy */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/textproxy.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _documentfragment__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../documentfragment */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/documentfragment.js");
/* harmony import */ var _nodelist__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../nodelist */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/nodelist.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/**
* @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 engine/model/operation/utils
*/
/**
* Contains functions used for composing model tree by {@link module:engine/model/operation/operation~Operation operations}.
* Those functions are built on top of {@link module:engine/model/node~Node node}, and it's child classes', APIs.
*
* @protected
* @namespace utils
*/
/**
* Inserts given nodes at given position.
*
* @protected
* @function module:engine/model/operation/utils~utils.insert
* @param {module:engine/model/position~Position} position Position at which nodes should be inserted.
* @param {module:engine/model/node~NodeSet} nodes Nodes to insert.
* @returns {module:engine/model/range~Range} Range spanning over inserted elements.
*/
function _insert( position, nodes ) {
nodes = _normalizeNodes( nodes );
// We have to count offset before inserting nodes because they can get merged and we would get wrong offsets.
const offset = nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
const parent = position.parent;
// Insertion might be in a text node, we should split it if that's the case.
_splitNodeAtPosition( position );
const index = position.index;
// Insert nodes at given index. After splitting we have a proper index and insertion is between nodes,
// using basic `Element` API.
parent._insertChild( index, nodes );
// Merge text nodes, if possible. Merging is needed only at points where inserted nodes "touch" "old" nodes.
_mergeNodesAtIndex( parent, index + nodes.length );
_mergeNodesAtIndex( parent, index );
return new _range__WEBPACK_IMPORTED_MODULE_3__["default"]( position, position.getShiftedBy( offset ) );
}
/**
* Removed nodes in given range. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted.
*
* @protected
* @function module:engine/model/operation/utils~utils._remove
* @param {module:engine/model/range~Range} range Range containing nodes to remove.
* @returns {Array.}
*/
function _remove( range ) {
if ( !range.isFlat ) {
/**
* Trying to remove a range which starts and ends in different element.
*
* @error operation-utils-remove-range-not-flat
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_6__["default"](
'operation-utils-remove-range-not-flat',
this
);
}
const parent = range.start.parent;
// Range may be inside text nodes, we have to split them if that's the case.
_splitNodeAtPosition( range.start );
_splitNodeAtPosition( range.end );
// Remove the text nodes using basic `Element` API.
const removed = parent._removeChildren( range.start.index, range.end.index - range.start.index );
// Merge text nodes, if possible. After some nodes were removed, node before and after removed range will be
// touching at the position equal to the removed range beginning. We check merging possibility there.
_mergeNodesAtIndex( parent, range.start.index );
return removed;
}
/**
* Moves nodes in given range to given target position. Only {@link module:engine/model/range~Range#isFlat flat} ranges are accepted.
*
* @protected
* @function module:engine/model/operation/utils~utils.move
* @param {module:engine/model/range~Range} sourceRange Range containing nodes to move.
* @param {module:engine/model/position~Position} targetPosition Position to which nodes should be moved.
* @returns {module:engine/model/range~Range} Range containing moved nodes.
*/
function _move( sourceRange, targetPosition ) {
if ( !sourceRange.isFlat ) {
/**
* Trying to move a range which starts and ends in different element.
*
* @error operation-utils-move-range-not-flat
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_6__["default"](
'operation-utils-move-range-not-flat',
this
);
}
const nodes = _remove( sourceRange );
// We have to fix `targetPosition` because model changed after nodes from `sourceRange` got removed and
// that change might have an impact on `targetPosition`.
targetPosition = targetPosition._getTransformedByDeletion( sourceRange.start, sourceRange.end.offset - sourceRange.start.offset );
return _insert( targetPosition, nodes );
}
/**
* Sets given attribute on nodes in given range. The attributes are only set on top-level nodes of the range, not on its children.
*
* @protected
* @function module:engine/model/operation/utils~utils._setAttribute
* @param {module:engine/model/range~Range} range Range containing nodes that should have the attribute set. Must be a flat range.
* @param {String} key Key of attribute to set.
* @param {*} value Attribute value.
*/
function _setAttribute( range, key, value ) {
// Range might start or end in text nodes, so we have to split them.
_splitNodeAtPosition( range.start );
_splitNodeAtPosition( range.end );
// Iterate over all items in the range.
for ( const item of range.getItems( { shallow: true } ) ) {
// Iterator will return `TextProxy` instances but we know that those text proxies will
// always represent full text nodes (this is guaranteed thanks to splitting we did before).
// So, we can operate on those text proxies' text nodes.
const node = item.is( '$textProxy' ) ? item.textNode : item;
if ( value !== null ) {
node._setAttribute( key, value );
} else {
node._removeAttribute( key );
}
// After attributes changing it may happen that some text nodes can be merged. Try to merge with previous node.
_mergeNodesAtIndex( node.parent, node.index );
}
// Try to merge last changed node with it's previous sibling (not covered by the loop above).
_mergeNodesAtIndex( range.end.parent, range.end.index );
}
/**
* Normalizes given object or an array of objects to an array of {@link module:engine/model/node~Node nodes}. See
* {@link module:engine/model/node~NodeSet NodeSet} for details on how normalization is performed.
*
* @protected
* @function module:engine/model/operation/utils~utils.normalizeNodes
* @param {module:engine/model/node~NodeSet} nodes Objects to normalize.
* @returns {Array.} Normalized nodes.
*/
function _normalizeNodes( nodes ) {
const normalized = [];
if ( !( nodes instanceof Array ) ) {
nodes = [ nodes ];
}
// Convert instances of classes other than Node.
for ( let i = 0; i < nodes.length; i++ ) {
if ( typeof nodes[ i ] == 'string' ) {
normalized.push( new _text__WEBPACK_IMPORTED_MODULE_1__["default"]( nodes[ i ] ) );
} else if ( nodes[ i ] instanceof _textproxy__WEBPACK_IMPORTED_MODULE_2__["default"] ) {
normalized.push( new _text__WEBPACK_IMPORTED_MODULE_1__["default"]( nodes[ i ].data, nodes[ i ].getAttributes() ) );
} else if ( nodes[ i ] instanceof _documentfragment__WEBPACK_IMPORTED_MODULE_4__["default"] || nodes[ i ] instanceof _nodelist__WEBPACK_IMPORTED_MODULE_5__["default"] ) {
for ( const child of nodes[ i ] ) {
normalized.push( child );
}
} else if ( nodes[ i ] instanceof _node__WEBPACK_IMPORTED_MODULE_0__["default"] ) {
normalized.push( nodes[ i ] );
}
// Skip unrecognized type.
}
// Merge text nodes.
for ( let i = 1; i < normalized.length; i++ ) {
const node = normalized[ i ];
const prev = normalized[ i - 1 ];
if ( node instanceof _text__WEBPACK_IMPORTED_MODULE_1__["default"] && prev instanceof _text__WEBPACK_IMPORTED_MODULE_1__["default"] && _haveSameAttributes( node, prev ) ) {
// Doing this instead changing `prev.data` because `data` is readonly.
normalized.splice( i - 1, 2, new _text__WEBPACK_IMPORTED_MODULE_1__["default"]( prev.data + node.data, prev.getAttributes() ) );
i--;
}
}
return normalized;
}
// Checks if nodes before and after given index in given element are {@link module:engine/model/text~Text text nodes} and
// merges them into one node if they have same attributes.
//
// Merging is done by removing two text nodes and inserting a new text node containing data from both merged text nodes.
//
// @private
// @param {module:engine/model/element~Element} element Parent element of nodes to merge.
// @param {Number} index Index between nodes to merge.
function _mergeNodesAtIndex( element, index ) {
const nodeBefore = element.getChild( index - 1 );
const nodeAfter = element.getChild( index );
// Check if both of those nodes are text objects with same attributes.
if ( nodeBefore && nodeAfter && nodeBefore.is( '$text' ) && nodeAfter.is( '$text' ) && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
// Append text of text node after index to the before one.
const mergedNode = new _text__WEBPACK_IMPORTED_MODULE_1__["default"]( nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes() );
// Remove separate text nodes.
element._removeChildren( index - 1, 2 );
// Insert merged text node.
element._insertChild( index - 1, mergedNode );
}
}
// Checks if given position is in a text node, and if so, splits the text node in two text nodes, each of them
// containing a part of original text node.
//
// @private
// @param {module:engine/model/position~Position} position Position at which node should be split.
function _splitNodeAtPosition( position ) {
const textNode = position.textNode;
const element = position.parent;
if ( textNode ) {
const offsetDiff = position.offset - textNode.startOffset;
const index = textNode.index;
element._removeChildren( index, 1 );
const firstPart = new _text__WEBPACK_IMPORTED_MODULE_1__["default"]( textNode.data.substr( 0, offsetDiff ), textNode.getAttributes() );
const secondPart = new _text__WEBPACK_IMPORTED_MODULE_1__["default"]( textNode.data.substr( offsetDiff ), textNode.getAttributes() );
element._insertChild( index, [ firstPart, secondPart ] );
}
}
// Checks whether two given nodes have same attributes.
//
// @private
// @param {module:engine/model/node~Node} nodeA Node to check.
// @param {module:engine/model/node~Node} nodeB Node to check.
// @returns {Boolean} `true` if nodes have same attributes, `false` otherwise.
function _haveSameAttributes( nodeA, nodeB ) {
const iteratorA = nodeA.getAttributes();
const iteratorB = nodeB.getAttributes();
for ( const attr of iteratorA ) {
if ( attr[ 1 ] !== nodeB.getAttribute( attr[ 0 ] ) ) {
return false;
}
iteratorB.next();
}
return iteratorB.next().done;
}
/**
* Value that can be normalized to an array of {@link module:engine/model/node~Node nodes}.
*
* Non-arrays are normalized as follows:
* * {@link module:engine/model/node~Node Node} is left as is,
* * {@link module:engine/model/textproxy~TextProxy TextProxy} and `String` are normalized to {@link module:engine/model/text~Text Text},
* * {@link module:engine/model/nodelist~NodeList NodeList} is normalized to an array containing all nodes that are in that node list,
* * {@link module:engine/model/documentfragment~DocumentFragment DocumentFragment} is normalized to an array containing all of it's
* * children.
*
* Arrays are processed item by item like non-array values and flattened to one array. Normalization always results in
* a flat array of {@link module:engine/model/node~Node nodes}. Consecutive text nodes (or items normalized to text nodes) will be
* merged if they have same attributes.
*
* @typedef {module:engine/model/node~Node|module:engine/model/textproxy~TextProxy|String|
* module:engine/model/nodelist~NodeList|module:engine/model/documentfragment~DocumentFragment|Iterable}
* module:engine/model/node~NodeSet
*/
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js":
/*!***********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js ***!
\***********************************************************************/
/*! exports provided: default, getTextNodeAtPosition, getNodeAfterPosition, getNodeBeforePosition */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Position; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getTextNodeAtPosition", function() { return getTextNodeAtPosition; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getNodeAfterPosition", function() { return getNodeAfterPosition; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getNodeBeforePosition", function() { return getNodeBeforePosition; });
/* harmony import */ var _treewalker__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./treewalker */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/treewalker.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/comparearrays */ "./node_modules/@ckeditor/ckeditor5-utils/src/comparearrays.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_version__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/version */ "./node_modules/@ckeditor/ckeditor5-utils/src/version.js");
/**
* @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 engine/model/position
*/
// To check if component is loaded more than once.
/**
* Represents a position in the model tree.
*
* A position is represented by its {@link module:engine/model/position~Position#root} and
* a {@link module:engine/model/position~Position#path} in that root.
*
* You can create position instances via its constructor or the `createPosition*()` factory methods of
* {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
*
* **Note:** Position is based on offsets, not indexes. This means that a position between two text nodes
* `foo` and `bar` has offset `3`, not `1`. See {@link module:engine/model/position~Position#path} for more information.
*
* Since a position in the model is represented by a {@link module:engine/model/position~Position#root position root} and
* {@link module:engine/model/position~Position#path position path} it is possible to create positions placed in non-existing places.
* This requirement is important for operational transformation algorithms.
*
* Also, {@link module:engine/model/operation/operation~Operation operations}
* kept in the {@link module:engine/model/document~Document#history document history}
* are storing positions (and ranges) which were correct when those operations were applied, but may not be correct
* after the document has changed.
*
* When changes are applied to the model, it may also happen that {@link module:engine/model/position~Position#parent position parent}
* will change even if position path has not changed. Keep in mind, that if a position leads to non-existing element,
* {@link module:engine/model/position~Position#parent} and some other properties and methods will throw errors.
*
* In most cases, position with wrong path is caused by an error in code, but it is sometimes needed, as described above.
*/
class Position {
/**
* Creates a position.
*
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
* @param {Array.} path Position path. See {@link module:engine/model/position~Position#path}.
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
* See {@link module:engine/model/position~PositionStickiness}.
*/
constructor( root, path, stickiness = 'toNone' ) {
if ( !root.is( 'element' ) && !root.is( 'documentFragment' ) ) {
/**
* Position root is invalid.
*
* Positions can only be anchored in elements or document fragments.
*
* @error model-position-root-invalid
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'model-position-root-invalid',
root
);
}
if ( !( path instanceof Array ) || path.length === 0 ) {
/**
* Position path must be an array with at least one item.
*
* @error model-position-path-incorrect-format
* @param path
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'model-position-path-incorrect-format',
root,
{ path }
);
}
// Normalize the root and path when element (not root) is passed.
if ( root.is( 'rootElement' ) ) {
path = path.slice();
} else {
path = [ ...root.getPath(), ...path ];
root = root.root;
}
/**
* Root of the position path.
*
* @readonly
* @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
* module:engine/model/position~Position#root
*/
this.root = root;
/**
* Position of the node in the tree. **Path contains offsets, not indexes.**
*
* Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
* {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
* {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
* down to the position offset in it's parent.
*
* ROOT
* |- P before: [ 0 ] after: [ 1 ]
* |- UL before: [ 1 ] after: [ 2 ]
* |- LI before: [ 1, 0 ] after: [ 1, 1 ]
* | |- foo before: [ 1, 0, 0 ] after: [ 1, 0, 3 ]
* |- LI before: [ 1, 1 ] after: [ 1, 2 ]
* |- bar before: [ 1, 1, 0 ] after: [ 1, 1, 3 ]
*
* `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
* greater than `1` you can place position offset between their start and end:
*
* ROOT
* |- P
* |- UL
* |- LI
* | |- f^o|o ^ has path: [ 1, 0, 1 ] | has path: [ 1, 0, 2 ]
* |- LI
* |- b^a|r ^ has path: [ 1, 1, 1 ] | has path: [ 1, 1, 2 ]
*
* @readonly
* @member {Array.} module:engine/model/position~Position#path
*/
this.path = path;
/**
* Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
*
* @member {module:engine/model/position~PositionStickiness} module:engine/model/position~Position#stickiness
*/
this.stickiness = stickiness;
}
/**
* Offset at which this position is located in its {@link module:engine/model/position~Position#parent parent}. It is equal
* to the last item in position {@link module:engine/model/position~Position#path path}.
*
* @type {Number}
*/
get offset() {
return this.path[ this.path.length - 1 ];
}
set offset( newOffset ) {
this.path[ this.path.length - 1 ] = newOffset;
}
/**
* Parent element of this position.
*
* Keep in mind that `parent` value is calculated when the property is accessed.
* If {@link module:engine/model/position~Position#path position path}
* leads to a non-existing element, `parent` property will throw error.
*
* Also it is a good idea to cache `parent` property if it is used frequently in an algorithm (i.e. in a long loop).
*
* @readonly
* @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
*/
get parent() {
let parent = this.root;
for ( let i = 0; i < this.path.length - 1; i++ ) {
parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) );
if ( !parent ) {
/**
* The position's path is incorrect. This means that a position does not point to
* a correct place in the tree and hence, some of its methods and getters cannot work correctly.
*
* **Note**: Unlike DOM and view positions, in the model, the
* {@link module:engine/model/position~Position#parent position's parent} is always an element or a document fragment.
* The last offset in the {@link module:engine/model/position~Position#path position's path} is the point in this element
* where this position points.
*
* Read more about model positions and offsets in
* the {@glink framework/guides/architecture/editing-engine#indexes-and-offsets Editing engine architecture guide}.
*
* @error model-position-path-incorrect
* @param {module:engine/model/position~Position} position The incorrect position.
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'model-position-path-incorrect', this, { position: this } );
}
}
if ( parent.is( '$text' ) ) {
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'model-position-path-incorrect', this, { position: this } );
}
return parent;
}
/**
* Position {@link module:engine/model/position~Position#offset offset} converted to an index in position's parent node. It is
* equal to the {@link module:engine/model/node~Node#index index} of a node after this position. If position is placed
* in text node, position index is equal to the index of that text node.
*
* @readonly
* @type {Number}
*/
get index() {
return this.parent.offsetToIndex( this.offset );
}
/**
* Returns {@link module:engine/model/text~Text text node} instance in which this position is placed or `null` if this
* position is not in a text node.
*
* @readonly
* @type {module:engine/model/text~Text|null}
*/
get textNode() {
return getTextNodeAtPosition( this, this.parent );
}
/**
* Node directly after this position or `null` if this position is in text node.
*
* @readonly
* @type {module:engine/model/node~Node|null}
*/
get nodeAfter() {
// Cache the parent and reuse for performance reasons. See #6579 and #6582.
const parent = this.parent;
return getNodeAfterPosition( this, parent, getTextNodeAtPosition( this, parent ) );
}
/**
* Node directly before this position or `null` if this position is in text node.
*
* @readonly
* @type {module:engine/model/node~Node|null}
*/
get nodeBefore() {
// Cache the parent and reuse for performance reasons. See #6579 and #6582.
const parent = this.parent;
return getNodeBeforePosition( this, parent, getTextNodeAtPosition( this, parent ) );
}
/**
* Is `true` if position is at the beginning of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isAtStart() {
return this.offset === 0;
}
/**
* Is `true` if position is at the end of its {@link module:engine/model/position~Position#parent parent}, `false` otherwise.
*
* @readonly
* @type {Boolean}
*/
get isAtEnd() {
return this.offset == this.parent.maxOffset;
}
/**
* Checks whether this position is before or after given position.
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {module:engine/model/position~PositionRelation}
*/
compareWith( otherPosition ) {
if ( this.root != otherPosition.root ) {
return 'different';
}
const result = Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__["default"])( this.path, otherPosition.path );
switch ( result ) {
case 'same':
return 'same';
case 'prefix':
return 'before';
case 'extension':
return 'after';
default:
return this.path[ result ] < otherPosition.path[ result ] ? 'before' : 'after';
}
}
/**
* Gets the farthest position which matches the callback using
* {@link module:engine/model/treewalker~TreeWalker TreeWalker}.
*
* For example:
*
* getLastMatchingPosition( value => value.type == 'text' );
* // []foo -> foo[]
*
* getLastMatchingPosition( value => value.type == 'text', { direction: 'backward' } );
* // foo[] -> []foo
*
* getLastMatchingPosition( value => false );
* // Do not move the position.
*
* @param {Function} skip Callback function. Gets {@link module:engine/model/treewalker~TreeWalkerValue} and should
* return `true` if the value should be skipped or `false` if not.
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
*
* @returns {module:engine/model/position~Position} The position after the last item which matches the `skip` callback test.
*/
getLastMatchingPosition( skip, options = {} ) {
options.startPosition = this;
const treeWalker = new _treewalker__WEBPACK_IMPORTED_MODULE_0__["default"]( options );
treeWalker.skip( skip );
return treeWalker.position;
}
/**
* Returns a path to this position's parent. Parent path is equal to position {@link module:engine/model/position~Position#path path}
* but without the last item.
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @returns {Array.} Path to the parent.
*/
getParentPath() {
return this.path.slice( 0, -1 );
}
/**
* Returns ancestors array of this position, that is this position's parent and its ancestors.
*
* @returns {Array.} Array with ancestors.
*/
getAncestors() {
const parent = this.parent;
if ( parent.is( 'documentFragment' ) ) {
return [ parent ];
} else {
return parent.getAncestors( { includeSelf: true } );
}
}
/**
* Returns the parent element of the given name. Returns null if the position is not inside the desired parent.
*
* @param {String} parentName The name of the parent element to find.
* @returns {module:engine/model/element~Element|null}
*/
findAncestor( parentName ) {
const parent = this.parent;
if ( parent.is( 'element' ) ) {
return parent.findAncestor( parentName, { includeSelf: true } );
}
return null;
}
/**
* Returns the slice of two position {@link #path paths} which is identical. The {@link #root roots}
* of these two paths must be identical.
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @param {module:engine/model/position~Position} position The second position.
* @returns {Array.} The common path.
*/
getCommonPath( position ) {
if ( this.root != position.root ) {
return [];
}
// We find on which tree-level start and end have the lowest common ancestor
const cmp = Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__["default"])( this.path, position.path );
// If comparison returned string it means that arrays are same.
const diffAt = ( typeof cmp == 'string' ) ? Math.min( this.path.length, position.path.length ) : cmp;
return this.path.slice( 0, diffAt );
}
/**
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of both positions. The {@link #root roots} of these two positions must be identical.
*
* @param {module:engine/model/position~Position} position The second position.
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor( position ) {
const ancestorsA = this.getAncestors();
const ancestorsB = position.getAncestors();
let i = 0;
while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
i++;
}
return i === 0 ? null : ancestorsA[ i - 1 ];
}
/**
* Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
* is shifted by `shift` value (can be a negative value).
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @param {Number} shift Offset shift. Can be a negative value.
* @returns {module:engine/model/position~Position} Shifted position.
*/
getShiftedBy( shift ) {
const shifted = this.clone();
const offset = shifted.offset + shift;
shifted.offset = offset < 0 ? 0 : offset;
return shifted;
}
/**
* Checks whether this position is after given position.
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @see module:engine/model/position~Position#isBefore
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if this position is after given position.
*/
isAfter( otherPosition ) {
return this.compareWith( otherPosition ) == 'after';
}
/**
* Checks whether this position is before given position.
*
* **Note:** watch out when using negation of the value returned by this method, because the negation will also
* be `true` if positions are in different roots and you might not expect this. You should probably use
* `a.isAfter( b ) || a.isEqual( b )` or `!a.isBefore( p ) && a.root == b.root` in most scenarios. If your
* condition uses multiple `isAfter` and `isBefore` checks, build them so they do not use negated values, i.e.:
*
* if ( a.isBefore( b ) && c.isAfter( d ) ) {
* // do A.
* } else {
* // do B.
* }
*
* or, if you have only one if-branch:
*
* if ( !( a.isBefore( b ) && c.isAfter( d ) ) {
* // do B.
* }
*
* rather than:
*
* if ( !a.isBefore( b ) || && !c.isAfter( d ) ) {
* // do B.
* } else {
* // do A.
* }
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if this position is before given position.
*/
isBefore( otherPosition ) {
return this.compareWith( otherPosition ) == 'before';
}
/**
* Checks whether this position is equal to given position.
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if positions are same.
*/
isEqual( otherPosition ) {
return this.compareWith( otherPosition ) == 'same';
}
/**
* Checks whether this position is touching given position. Positions touch when there are no text nodes
* or empty nodes in a range between them. Technically, those positions are not equal but in many cases
* they are very similar or even indistinguishable.
*
* @param {module:engine/model/position~Position} otherPosition Position to compare with.
* @returns {Boolean} True if positions touch.
*/
isTouching( otherPosition ) {
let left = null;
let right = null;
const compare = this.compareWith( otherPosition );
switch ( compare ) {
case 'same':
return true;
case 'before':
left = Position._createAt( this );
right = Position._createAt( otherPosition );
break;
case 'after':
left = Position._createAt( otherPosition );
right = Position._createAt( this );
break;
default:
return false;
}
// Cached for optimization purposes.
let leftParent = left.parent;
while ( left.path.length + right.path.length ) {
if ( left.isEqual( right ) ) {
return true;
}
if ( left.path.length > right.path.length ) {
if ( left.offset !== leftParent.maxOffset ) {
return false;
}
left.path = left.path.slice( 0, -1 );
leftParent = leftParent.parent;
left.offset++;
} else {
if ( right.offset !== 0 ) {
return false;
}
right.path = right.path.slice( 0, -1 );
}
}
}
/**
* Checks whether this object is of the given.
*
* position.is( 'position' ); // -> true
* position.is( 'model:position' ); // -> true
*
* position.is( 'view:position' ); // -> false
* position.is( 'documentSelection' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'position' || type === 'model:position';
}
/**
* Checks if two positions are in the same parent.
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @param {module:engine/model/position~Position} position Position to compare with.
* @returns {Boolean} `true` if positions have the same parent, `false` otherwise.
*/
hasSameParentAs( position ) {
if ( this.root !== position.root ) {
return false;
}
const thisParentPath = this.getParentPath();
const posParentPath = position.getParentPath();
return Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__["default"])( thisParentPath, posParentPath ) == 'same';
}
/**
* Returns a copy of this position that is transformed by given `operation`.
*
* The new position's parameters are updated accordingly to the effect of the `operation`.
*
* For example, if `n` nodes are inserted before the position, the returned position {@link ~Position#offset} will be
* increased by `n`. If the position was in a merged element, it will be accordingly moved to the new element, etc.
*
* This method is safe to use it on non-existing positions (for example during operational transformation).
*
* @param {module:engine/model/operation/operation~Operation} operation Operation to transform by.
* @returns {module:engine/model/position~Position} Transformed position.
*/
getTransformedByOperation( operation ) {
let result;
switch ( operation.type ) {
case 'insert':
result = this._getTransformedByInsertOperation( operation );
break;
case 'move':
case 'remove':
case 'reinsert':
result = this._getTransformedByMoveOperation( operation );
break;
case 'split':
result = this._getTransformedBySplitOperation( operation );
break;
case 'merge':
result = this._getTransformedByMergeOperation( operation );
break;
default:
result = Position._createAt( this );
break;
}
return result;
}
/**
* Returns a copy of this position transformed by an insert operation.
*
* @protected
* @param {module:engine/model/operation/insertoperation~InsertOperation} operation
* @returns {module:engine/model/position~Position}
*/
_getTransformedByInsertOperation( operation ) {
return this._getTransformedByInsertion( operation.position, operation.howMany );
}
/**
* Returns a copy of this position transformed by a move operation.
*
* @protected
* @param {module:engine/model/operation/moveoperation~MoveOperation} operation
* @returns {module:engine/model/position~Position}
*/
_getTransformedByMoveOperation( operation ) {
return this._getTransformedByMove( operation.sourcePosition, operation.targetPosition, operation.howMany );
}
/**
* Returns a copy of this position transformed by a split operation.
*
* @protected
* @param {module:engine/model/operation/splitoperation~SplitOperation} operation
* @returns {module:engine/model/position~Position}
*/
_getTransformedBySplitOperation( operation ) {
const movedRange = operation.movedRange;
const isContained = movedRange.containsPosition( this ) ||
( movedRange.start.isEqual( this ) && this.stickiness == 'toNext' );
if ( isContained ) {
return this._getCombined( operation.splitPosition, operation.moveTargetPosition );
} else {
if ( operation.graveyardPosition ) {
return this._getTransformedByMove( operation.graveyardPosition, operation.insertionPosition, 1 );
} else {
return this._getTransformedByInsertion( operation.insertionPosition, 1 );
}
}
}
/**
* Returns a copy of this position transformed by merge operation.
*
* @protected
* @param {module:engine/model/operation/mergeoperation~MergeOperation} operation
* @returns {module:engine/model/position~Position}
*/
_getTransformedByMergeOperation( operation ) {
const movedRange = operation.movedRange;
const isContained = movedRange.containsPosition( this ) || movedRange.start.isEqual( this );
let pos;
if ( isContained ) {
pos = this._getCombined( operation.sourcePosition, operation.targetPosition );
if ( operation.sourcePosition.isBefore( operation.targetPosition ) ) {
// Above happens during OT when the merged element is moved before the merged-to element.
pos = pos._getTransformedByDeletion( operation.deletionPosition, 1 );
}
} else if ( this.isEqual( operation.deletionPosition ) ) {
pos = Position._createAt( operation.deletionPosition );
} else {
pos = this._getTransformedByMove( operation.deletionPosition, operation.graveyardPosition, 1 );
}
return pos;
}
/**
* Returns a copy of this position that is updated by removing `howMany` nodes starting from `deletePosition`.
* It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
*
* @protected
* @param {module:engine/model/position~Position} deletePosition Position before the first removed node.
* @param {Number} howMany How many nodes are removed.
* @returns {module:engine/model/position~Position|null} Transformed position or `null`.
*/
_getTransformedByDeletion( deletePosition, howMany ) {
const transformed = Position._createAt( this );
// This position can't be affected if deletion was in a different root.
if ( this.root != deletePosition.root ) {
return transformed;
}
if ( Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__["default"])( deletePosition.getParentPath(), this.getParentPath() ) == 'same' ) {
// If nodes are removed from the node that is pointed by this position...
if ( deletePosition.offset < this.offset ) {
// And are removed from before an offset of that position...
if ( deletePosition.offset + howMany > this.offset ) {
// Position is in removed range, it's no longer in the tree.
return null;
} else {
// Decrement the offset accordingly.
transformed.offset -= howMany;
}
}
} else if ( Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__["default"])( deletePosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
// If nodes are removed from a node that is on a path to this position...
const i = deletePosition.path.length - 1;
if ( deletePosition.offset <= this.path[ i ] ) {
// And are removed from before next node of that path...
if ( deletePosition.offset + howMany > this.path[ i ] ) {
// If the next node of that path is removed return null
// because the node containing this position got removed.
return null;
} else {
// Otherwise, decrement index on that path.
transformed.path[ i ] -= howMany;
}
}
}
return transformed;
}
/**
* Returns a copy of this position that is updated by inserting `howMany` nodes at `insertPosition`.
*
* @protected
* @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
* @param {Number} howMany How many nodes are inserted.
* @returns {module:engine/model/position~Position} Transformed position.
*/
_getTransformedByInsertion( insertPosition, howMany ) {
const transformed = Position._createAt( this );
// This position can't be affected if insertion was in a different root.
if ( this.root != insertPosition.root ) {
return transformed;
}
if ( Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__["default"])( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
// If nodes are inserted in the node that is pointed by this position...
if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && this.stickiness != 'toPrevious' ) ) {
// And are inserted before an offset of that position...
// "Push" this positions offset.
transformed.offset += howMany;
}
} else if ( Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_1__["default"])( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
// If nodes are inserted in a node that is on a path to this position...
const i = insertPosition.path.length - 1;
if ( insertPosition.offset <= this.path[ i ] ) {
// And are inserted before next node of that path...
// "Push" the index on that path.
transformed.path[ i ] += howMany;
}
}
return transformed;
}
/**
* Returns a copy of this position that is updated by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
*
* @protected
* @param {module:engine/model/position~Position} sourcePosition Position before the first element to move.
* @param {module:engine/model/position~Position} targetPosition Position where moved elements will be inserted.
* @param {Number} howMany How many consecutive nodes to move, starting from `sourcePosition`.
* @returns {module:engine/model/position~Position} Transformed position.
*/
_getTransformedByMove( sourcePosition, targetPosition, howMany ) {
// Update target position, as it could be affected by nodes removal.
targetPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
if ( sourcePosition.isEqual( targetPosition ) ) {
// If `targetPosition` is equal to `sourcePosition` this isn't really any move. Just return position as it is.
return Position._createAt( this );
}
// Moving a range removes nodes from their original position. We acknowledge this by proper transformation.
const transformed = this._getTransformedByDeletion( sourcePosition, howMany );
const isMoved = transformed === null ||
( sourcePosition.isEqual( this ) && this.stickiness == 'toNext' ) ||
( sourcePosition.getShiftedBy( howMany ).isEqual( this ) && this.stickiness == 'toPrevious' );
if ( isMoved ) {
// This position is inside moved range (or sticks to it).
// In this case, we calculate a combination of this position, move source position and target position.
return this._getCombined( sourcePosition, targetPosition );
} else {
// This position is not inside a removed range.
//
// In next step, we simply reflect inserting `howMany` nodes, which might further affect the position.
return transformed._getTransformedByInsertion( targetPosition, howMany );
}
}
/**
* Returns a new position that is a combination of this position and given positions.
*
* The combined position is a copy of this position transformed by moving a range starting at `source` position
* to the `target` position. It is expected that this position is inside the moved range.
*
* Example:
*
* let original = model.createPositionFromPath( root, [ 2, 3, 1 ] );
* let source = model.createPositionFromPath( root, [ 2, 2 ] );
* let target = model.createPositionFromPath( otherRoot, [ 1, 1, 3 ] );
* original._getCombined( source, target ); // path is [ 1, 1, 4, 1 ], root is `otherRoot`
*
* Explanation:
*
* We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position
* was inside moved nodes and now should point to the new place. The moved nodes will be after
* positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node,
* the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we
* took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path.
* Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`.
*
* @protected
* @param {module:engine/model/position~Position} source Beginning of the moved range.
* @param {module:engine/model/position~Position} target Position where the range is moved.
* @returns {module:engine/model/position~Position} Combined position.
*/
_getCombined( source, target ) {
const i = source.path.length - 1;
// The first part of a path to combined position is a path to the place where nodes were moved.
const combined = Position._createAt( target );
combined.stickiness = this.stickiness;
// Then we have to update the rest of the path.
// Fix the offset because this position might be after `from` position and we have to reflect that.
combined.offset = combined.offset + this.path[ i ] - source.offset;
// Then, add the rest of the path.
// If this position is at the same level as `from` position nothing will get added.
combined.path = [ ...combined.path, ...this.path.slice( i + 1 ) ];
return combined;
}
/**
* @inheritDoc
*/
toJSON() {
return {
root: this.root.toJSON(),
path: Array.from( this.path ),
stickiness: this.stickiness
};
}
/**
* Returns a new position that is equal to current position.
*
* @returns {module:engine/model/position~Position}
*/
clone() {
return new this.constructor( this.root, this.path, this.stickiness );
}
/**
* Creates position at the given location. The location can be specified as:
*
* * a {@link module:engine/model/position~Position position},
* * parent element and offset (offset defaults to `0`),
* * parent element and `'end'` (sets position at the end of that element),
* * {@link module:engine/model/item~Item model item} and `'before'` or `'after'` (sets position before or after given model item).
*
* This method is a shortcut to other factory methods such as:
*
* * {@link module:engine/model/position~Position._createBefore},
* * {@link module:engine/model/position~Position._createAfter}.
*
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when the
* first parameter is a {@link module:engine/model/item~Item model item}.
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness. Used only when the
* first parameter is a {@link module:engine/model/item~Item model item}.
* @protected
*/
static _createAt( itemOrPosition, offset, stickiness = 'toNone' ) {
if ( itemOrPosition instanceof Position ) {
return new Position( itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness );
} else {
const node = itemOrPosition;
if ( offset == 'end' ) {
offset = node.maxOffset;
} else if ( offset == 'before' ) {
return this._createBefore( node, stickiness );
} else if ( offset == 'after' ) {
return this._createAfter( node, stickiness );
} else if ( offset !== 0 && !offset ) {
/**
* {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
* requires the offset to be specified when the first parameter is a model item.
*
* @error model-createpositionat-offset-required
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"]( 'model-createpositionat-offset-required', [ this, itemOrPosition ] );
}
if ( !node.is( 'element' ) && !node.is( 'documentFragment' ) ) {
/**
* Position parent have to be a model element or model document fragment.
*
* @error model-position-parent-incorrect
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'model-position-parent-incorrect',
[ this, itemOrPosition ]
);
}
const path = node.getPath();
path.push( offset );
return new this( node.root, path, stickiness );
}
}
/**
* Creates a new position, after given {@link module:engine/model/item~Item model item}.
*
* @param {module:engine/model/item~Item} item Item after which the position should be placed.
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
* @returns {module:engine/model/position~Position}
* @protected
*/
static _createAfter( item, stickiness ) {
if ( !item.parent ) {
/**
* You can not make a position after a root element.
*
* @error model-position-after-root
* @param {module:engine/model/item~Item} root
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'model-position-after-root',
[ this, item ],
{ root: item }
);
}
return this._createAt( item.parent, item.endOffset, stickiness );
}
/**
* Creates a new position, before the given {@link module:engine/model/item~Item model item}.
*
* @param {module:engine/model/item~Item} item Item before which the position should be placed.
* @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
* @returns {module:engine/model/position~Position}
* @protected
*/
static _createBefore( item, stickiness ) {
if ( !item.parent ) {
/**
* You can not make a position before a root element.
*
* @error model-position-before-root
* @param {module:engine/model/item~Item} root
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'model-position-before-root',
item,
{ root: item }
);
}
return this._createAt( item.parent, item.startOffset, stickiness );
}
/**
* Creates a `Position` instance from given plain object (i.e. parsed JSON string).
*
* @param {Object} json Plain object to be converted to `Position`.
* @param {module:engine/model/document~Document} doc Document object that will be position owner.
* @returns {module:engine/model/position~Position} `Position` instance created using given plain object.
*/
static fromJSON( json, doc ) {
if ( json.root === '$graveyard' ) {
const pos = new Position( doc.graveyard, json.path );
pos.stickiness = json.stickiness;
return pos;
}
if ( !doc.getRoot( json.root ) ) {
/**
* Cannot create position for document. Root with specified name does not exist.
*
* @error model-position-fromjson-no-root
* @param {String} rootName
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'model-position-fromjson-no-root',
doc,
{ rootName: json.root }
);
}
return new Position( doc.getRoot( json.root ), json.path, json.stickiness );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `${ this.root } [ ${ this.path.join( ', ' ) } ]`;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // log() {
// @if CK_DEBUG_ENGINE // console.log( 'ModelPosition: ' + this );
// @if CK_DEBUG_ENGINE // }
}
/**
* A flag indicating whether this position is `'before'` or `'after'` or `'same'` as given position.
* If positions are in different roots `'different'` flag is returned.
*
* @typedef {String} module:engine/model/position~PositionRelation
*/
/**
* Represents how position is "sticking" with neighbour nodes. Used to define how position should be transformed (moved)
* in edge cases. Possible values: `'toNone'`, `'toNext'`, `'toPrevious'`.
*
* Examples:
*
* Insert. Position is at | and nodes are inserted at the same position, marked as ^:
*
* - sticks to none: f^|oo
-> fbar|oo
* - sticks to next node: f^|oo
-> fbar|oo
* - sticks to previous node: f|^oo
-> f|baroo
*
*
* Move. Position is at | and range [oo] is moved to position ^:
*
* - sticks to none: f|[oo]
b^ar
-> f|
booar
* - sticks to none: f[oo]|
b^ar
-> f|
booar
*
* - sticks to next node: f|[oo]
b^ar
-> f
b|ooar
* - sticks to next node: f[oo]|
b^ar
-> f|
booar
*
* - sticks to previous node: f|[oo]
b^ar
-> f|
booar
* - sticks to previous node: f[oo]|
b^ar
-> f
boo|ar
*
* @typedef {String} module:engine/model/position~PositionStickiness
*/
/**
* Returns a text node at the given position.
*
* This is a helper function optimized to reuse the position parent instance for performance reasons.
*
* Normally, you should use {@link module:engine/model/position~Position#textNode `Position#textNode`}.
* If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`}
* check if your algorithm does not access it multiple times (which can happen directly or indirectly via other position properties).
*
* See https://github.com/ckeditor/ckeditor5/issues/6579.
*
* See also:
*
* * {@link module:engine/model/position~getNodeAfterPosition}
* * {@link module:engine/model/position~getNodeBeforePosition}
*
* @param {module:engine/model/position~Position} position
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} positionParent The parent of the
* given position.
* @returns {module:engine/model/text~Text|null}
*/
function getTextNodeAtPosition( position, positionParent ) {
const node = positionParent.getChild( positionParent.offsetToIndex( position.offset ) );
if ( node && node.is( '$text' ) && node.startOffset < position.offset ) {
return node;
}
return null;
}
/**
* Returns the node after the given position.
*
* This is a helper function optimized to reuse the position parent instance and the calculation of the text node at the
* specific position for performance reasons.
*
* Normally, you should use {@link module:engine/model/position~Position#nodeAfter `Position#nodeAfter`}.
* If you start hitting performance issues with {@link module:engine/model/position~Position#parent `Position#parent`} and/or
* {@link module:engine/model/position~Position#textNode `Position#textNode`}
* check if your algorithm does not access those properties multiple times
* (which can happen directly or indirectly via other position properties).
*
* See https://github.com/ckeditor/ckeditor5/issues/6579 and https://github.com/ckeditor/ckeditor5/issues/6582.
*
* See also:
*
* * {@link module:engine/model/position~getTextNodeAtPosition}
* * {@link module:engine/model/position~getNodeBeforePosition}
*
* @param {module:engine/model/position~Position} position
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} positionParent The parent of the
* given position.
* @param {module:engine/model/text~Text|null} textNode Text node at the given position.
* @returns {module:engine/model/node~Node|null}
*/
function getNodeAfterPosition( position, positionParent, textNode ) {
if ( textNode !== null ) {
return null;
}
return positionParent.getChild( positionParent.offsetToIndex( position.offset ) );
}
/**
* Returns the node before the given position.
*
* Refer to {@link module:engine/model/position~getNodeBeforePosition} for documentation on when to use this util method.
*
* See also:
*
* * {@link module:engine/model/position~getTextNodeAtPosition}
* * {@link module:engine/model/position~getNodeAfterPosition}
*
* @param {module:engine/model/position~Position} position
* @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} positionParent The parent of the
* given position.
* @param {module:engine/model/text~Text|null} textNode Text node at the given position.
* @returns {module:engine/model/node~Node|null}
*/
function getNodeBeforePosition( position, positionParent, textNode ) {
if ( textNode !== null ) {
return null;
}
return positionParent.getChild( positionParent.offsetToIndex( position.offset ) - 1 );
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js":
/*!********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js ***!
\********************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Range; });
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _treewalker__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./treewalker */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/treewalker.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/comparearrays */ "./node_modules/@ckeditor/ckeditor5-utils/src/comparearrays.js");
/**
* @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 engine/model/range
*/
/**
* Represents a range in the model tree.
*
* A range is defined by its {@link module:engine/model/range~Range#start} and {@link module:engine/model/range~Range#end}
* positions.
*
* You can create range instances via its constructor or the `createRange*()` factory methods of
* {@link module:engine/model/model~Model} and {@link module:engine/model/writer~Writer}.
*/
class Range {
/**
* Creates a range spanning from `start` position to `end` position.
*
* @param {module:engine/model/position~Position} start Start position.
* @param {module:engine/model/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
*/
constructor( start, end = null ) {
/**
* Start position.
*
* @readonly
* @member {module:engine/model/position~Position}
*/
this.start = _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( start );
/**
* End position.
*
* @readonly
* @member {module:engine/model/position~Position}
*/
this.end = end ? _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( end ) : _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( start );
// If the range is collapsed, treat in a similar way as a position and set its boundaries stickiness to 'toNone'.
// In other case, make the boundaries stick to the "inside" of the range.
this.start.stickiness = this.isCollapsed ? 'toNone' : 'toNext';
this.end.stickiness = this.isCollapsed ? 'toNone' : 'toPrevious';
}
/**
* Iterable interface.
*
* Iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
* them together with additional information like length or {@link module:engine/model/position~Position positions},
* grouped as {@link module:engine/model/treewalker~TreeWalkerValue}.
* It iterates over all {@link module:engine/model/textproxy~TextProxy text contents} that are inside the range
* and all the {@link module:engine/model/element~Element}s that are entered into when iterating over this range.
*
* This iterator uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range
* and `ignoreElementEnd` option set to `true`.
*
* @returns {Iterable.}
*/
* [ Symbol.iterator ]() {
yield* new _treewalker__WEBPACK_IMPORTED_MODULE_1__["default"]( { boundaries: this, ignoreElementEnd: true } );
}
/**
* Returns whether the range is collapsed, that is if {@link #start} and
* {@link #end} positions are equal.
*
* @type {Boolean}
*/
get isCollapsed() {
return this.start.isEqual( this.end );
}
/**
* Returns whether this range is flat, that is if {@link #start} position and
* {@link #end} position are in the same {@link module:engine/model/position~Position#parent}.
*
* @type {Boolean}
*/
get isFlat() {
const startParentPath = this.start.getParentPath();
const endParentPath = this.end.getParentPath();
return Object(_ckeditor_ckeditor5_utils_src_comparearrays__WEBPACK_IMPORTED_MODULE_3__["default"])( startParentPath, endParentPath ) == 'same';
}
/**
* Range root element.
*
* @type {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
*/
get root() {
return this.start.root;
}
/**
* Checks whether this range contains given {@link module:engine/model/position~Position position}.
*
* @param {module:engine/model/position~Position} position Position to check.
* @returns {Boolean} `true` if given {@link module:engine/model/position~Position position} is contained
* in this range,`false` otherwise.
*/
containsPosition( position ) {
return position.isAfter( this.start ) && position.isBefore( this.end );
}
/**
* Checks whether this range contains given {@link ~Range range}.
*
* @param {module:engine/model/range~Range} otherRange Range to check.
* @param {Boolean} [loose=false] Whether the check is loose or strict. If the check is strict (`false`), compared range cannot
* start or end at the same position as this range boundaries. If the check is loose (`true`), compared range can start, end or
* even be equal to this range. Note that collapsed ranges are always compared in strict mode.
* @returns {Boolean} `true` if given {@link ~Range range} boundaries are contained by this range, `false` otherwise.
*/
containsRange( otherRange, loose = false ) {
if ( otherRange.isCollapsed ) {
loose = false;
}
const containsStart = this.containsPosition( otherRange.start ) || ( loose && this.start.isEqual( otherRange.start ) );
const containsEnd = this.containsPosition( otherRange.end ) || ( loose && this.end.isEqual( otherRange.end ) );
return containsStart && containsEnd;
}
/**
* Checks whether given {@link module:engine/model/item~Item} is inside this range.
*
* @param {module:engine/model/item~Item} item Model item to check.
*/
containsItem( item ) {
const pos = _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createBefore( item );
return this.containsPosition( pos ) || this.start.isEqual( pos );
}
/**
* Checks whether this object is of the given.
*
* range.is( 'range' ); // -> true
* range.is( 'model:range' ); // -> true
*
* range.is( 'view:range' ); // -> false
* range.is( 'documentSelection' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'range' || type === 'model:range';
}
/**
* Two ranges are equal if their {@link #start} and {@link #end} positions are equal.
*
* @param {module:engine/model/range~Range} otherRange Range to compare with.
* @returns {Boolean} `true` if ranges are equal, `false` otherwise.
*/
isEqual( otherRange ) {
return this.start.isEqual( otherRange.start ) && this.end.isEqual( otherRange.end );
}
/**
* Checks and returns whether this range intersects with given range.
*
* @param {module:engine/model/range~Range} otherRange Range to compare with.
* @returns {Boolean} `true` if ranges intersect, `false` otherwise.
*/
isIntersecting( otherRange ) {
return this.start.isBefore( otherRange.end ) && this.end.isAfter( otherRange.start );
}
/**
* Computes which part(s) of this {@link ~Range range} is not a part of given {@link ~Range range}.
* Returned array contains zero, one or two {@link ~Range ranges}.
*
* Examples:
*
* let range = model.createRange(
* model.createPositionFromPath( root, [ 2, 7 ] ),
* model.createPositionFromPath( root, [ 4, 0, 1 ] )
* );
* let otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 5 ] ) );
* let transformed = range.getDifference( otherRange );
* // transformed array has no ranges because `otherRange` contains `range`
*
* otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 3 ] ) );
* transformed = range.getDifference( otherRange );
* // transformed array has one range: from [ 3 ] to [ 4, 0, 1 ]
*
* otherRange = model.createRange( model.createPositionFromPath( root, [ 3 ] ), model.createPositionFromPath( root, [ 4 ] ) );
* transformed = range.getDifference( otherRange );
* // transformed array has two ranges: from [ 2, 7 ] to [ 3 ] and from [ 4 ] to [ 4, 0, 1 ]
*
* @param {module:engine/model/range~Range} otherRange Range to differentiate against.
* @returns {Array.} The difference between ranges.
*/
getDifference( otherRange ) {
const ranges = [];
if ( this.isIntersecting( otherRange ) ) {
// Ranges intersect.
if ( this.containsPosition( otherRange.start ) ) {
// Given range start is inside this range. This means that we have to
// add shrunken range - from the start to the middle of this range.
ranges.push( new Range( this.start, otherRange.start ) );
}
if ( this.containsPosition( otherRange.end ) ) {
// Given range end is inside this range. This means that we have to
// add shrunken range - from the middle of this range to the end.
ranges.push( new Range( otherRange.end, this.end ) );
}
} else {
// Ranges do not intersect, return the original range.
ranges.push( new Range( this.start, this.end ) );
}
return ranges;
}
/**
* Returns an intersection of this {@link ~Range range} and given {@link ~Range range}.
* Intersection is a common part of both of those ranges. If ranges has no common part, returns `null`.
*
* Examples:
*
* let range = model.createRange(
* model.createPositionFromPath( root, [ 2, 7 ] ),
* model.createPositionFromPath( root, [ 4, 0, 1 ] )
* );
* let otherRange = model.createRange( model.createPositionFromPath( root, [ 1 ] ), model.createPositionFromPath( root, [ 2 ] ) );
* let transformed = range.getIntersection( otherRange ); // null - ranges have no common part
*
* otherRange = model.createRange( model.createPositionFromPath( root, [ 3 ] ), model.createPositionFromPath( root, [ 5 ] ) );
* transformed = range.getIntersection( otherRange ); // range from [ 3 ] to [ 4, 0, 1 ]
*
* @param {module:engine/model/range~Range} otherRange Range to check for intersection.
* @returns {module:engine/model/range~Range|null} A common part of given ranges or `null` if ranges have no common part.
*/
getIntersection( otherRange ) {
if ( this.isIntersecting( otherRange ) ) {
// Ranges intersect, so a common range will be returned.
// At most, it will be same as this range.
let commonRangeStart = this.start;
let commonRangeEnd = this.end;
if ( this.containsPosition( otherRange.start ) ) {
// Given range start is inside this range. This means thaNt we have to
// shrink common range to the given range start.
commonRangeStart = otherRange.start;
}
if ( this.containsPosition( otherRange.end ) ) {
// Given range end is inside this range. This means that we have to
// shrink common range to the given range end.
commonRangeEnd = otherRange.end;
}
return new Range( commonRangeStart, commonRangeEnd );
}
// Ranges do not intersect, so they do not have common part.
return null;
}
/**
* Returns a range created by joining this {@link ~Range range} with the given {@link ~Range range}.
* If ranges have no common part, returns `null`.
*
* Examples:
*
* let range = model.createRange(
* model.createPositionFromPath( root, [ 2, 7 ] ),
* model.createPositionFromPath( root, [ 4, 0, 1 ] )
* );
* let otherRange = model.createRange(
* model.createPositionFromPath( root, [ 1 ] ),
* model.createPositionFromPath( root, [ 2 ] )
* );
* let transformed = range.getJoined( otherRange ); // null - ranges have no common part
*
* otherRange = model.createRange(
* model.createPositionFromPath( root, [ 3 ] ),
* model.createPositionFromPath( root, [ 5 ] )
* );
* transformed = range.getJoined( otherRange ); // range from [ 2, 7 ] to [ 5 ]
*
* @param {module:engine/model/range~Range} otherRange Range to be joined.
* @param {Boolean} [loose=false] Whether the intersection check is loose or strict. If the check is strict (`false`),
* ranges are tested for intersection or whether start/end positions are equal. If the check is loose (`true`),
* compared range is also checked if it's {@link module:engine/model/position~Position#isTouching touching} current range.
* @returns {module:engine/model/range~Range|null} A sum of given ranges or `null` if ranges have no common part.
*/
getJoined( otherRange, loose = false ) {
let shouldJoin = this.isIntersecting( otherRange );
if ( !shouldJoin ) {
if ( this.start.isBefore( otherRange.start ) ) {
shouldJoin = loose ? this.end.isTouching( otherRange.start ) : this.end.isEqual( otherRange.start );
} else {
shouldJoin = loose ? otherRange.end.isTouching( this.start ) : otherRange.end.isEqual( this.start );
}
}
if ( !shouldJoin ) {
return null;
}
let startPosition = this.start;
let endPosition = this.end;
if ( otherRange.start.isBefore( startPosition ) ) {
startPosition = otherRange.start;
}
if ( otherRange.end.isAfter( endPosition ) ) {
endPosition = otherRange.end;
}
return new Range( startPosition, endPosition );
}
/**
* Computes and returns the smallest set of {@link #isFlat flat} ranges, that covers this range in whole.
*
* See an example of a model structure (`[` and `]` are range boundaries):
*
* root root
* |- element DIV DIV P2 P3 DIV
* | |- element H H P1 f o o b a r H P4
* | | |- "fir[st" fir[st lorem se]cond ipsum
* | |- element P1
* | | |- "lorem" ||
* |- element P2 ||
* | |- "foo" VV
* |- element P3
* | |- "bar" root
* |- element DIV DIV [P2 P3] DIV
* | |- element H H [P1] f o o b a r H P4
* | | |- "se]cond" fir[st] lorem [se]cond ipsum
* | |- element P4
* | | |- "ipsum"
*
* As it can be seen, letters contained in the range are: `stloremfoobarse`, spread across different parents.
* We are looking for minimal set of flat ranges that contains the same nodes.
*
* Minimal flat ranges for above range `( [ 0, 0, 3 ], [ 3, 0, 2 ] )` will be:
*
* ( [ 0, 0, 3 ], [ 0, 0, 5 ] ) = "st"
* ( [ 0, 1 ], [ 0, 2 ] ) = element P1 ("lorem")
* ( [ 1 ], [ 3 ] ) = element P2, element P3 ("foobar")
* ( [ 3, 0, 0 ], [ 3, 0, 2 ] ) = "se"
*
* **Note:** if an {@link module:engine/model/element~Element element} is not wholly contained in this range, it won't be returned
* in any of the returned flat ranges. See in the example how `H` elements at the beginning and at the end of the range
* were omitted. Only their parts that were wholly in the range were returned.
*
* **Note:** this method is not returning flat ranges that contain no nodes.
*
* @returns {Array.} Array of flat ranges covering this range.
*/
getMinimalFlatRanges() {
const ranges = [];
const diffAt = this.start.getCommonPath( this.end ).length;
const pos = _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( this.start );
let posParent = pos.parent;
// Go up.
while ( pos.path.length > diffAt + 1 ) {
const howMany = posParent.maxOffset - pos.offset;
if ( howMany !== 0 ) {
ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
}
pos.path = pos.path.slice( 0, -1 );
pos.offset++;
posParent = posParent.parent;
}
// Go down.
while ( pos.path.length <= this.end.path.length ) {
const offset = this.end.path[ pos.path.length - 1 ];
const howMany = offset - pos.offset;
if ( howMany !== 0 ) {
ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
}
pos.offset = offset;
pos.path.push( 0 );
}
return ranges;
}
/**
* Creates a {@link module:engine/model/treewalker~TreeWalker TreeWalker} instance with this range as a boundary.
*
* For example, to iterate over all items in the entire document root:
*
* // Create a range spanning over the entire root content:
* const range = editor.model.createRangeIn( editor.model.document.getRoot() );
*
* // Iterate over all items in this range:
* for ( const value of range.getWalker() ) {
* console.log( value.item );
* }
*
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
* @param {module:engine/model/position~Position} [options.startPosition]
* @param {Boolean} [options.singleCharacters=false]
* @param {Boolean} [options.shallow=false]
* @param {Boolean} [options.ignoreElementEnd=false]
*/
getWalker( options = {} ) {
options.boundaries = this;
return new _treewalker__WEBPACK_IMPORTED_MODULE_1__["default"]( options );
}
/**
* Returns an iterator that iterates over all {@link module:engine/model/item~Item items} that are in this range and returns
* them.
*
* This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range and `ignoreElementEnd` option
* set to `true`. However it returns only {@link module:engine/model/item~Item model items},
* not {@link module:engine/model/treewalker~TreeWalkerValue}.
*
* You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for
* a full list of available options.
*
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
* @returns {Iterable.}
*/
* getItems( options = {} ) {
options.boundaries = this;
options.ignoreElementEnd = true;
const treeWalker = new _treewalker__WEBPACK_IMPORTED_MODULE_1__["default"]( options );
for ( const value of treeWalker ) {
yield value.item;
}
}
/**
* Returns an iterator that iterates over all {@link module:engine/model/position~Position positions} that are boundaries or
* contained in this range.
*
* This method uses {@link module:engine/model/treewalker~TreeWalker} with `boundaries` set to this range. However it returns only
* {@link module:engine/model/position~Position positions}, not {@link module:engine/model/treewalker~TreeWalkerValue}.
*
* You may specify additional options for the tree walker. See {@link module:engine/model/treewalker~TreeWalker} for
* a full list of available options.
*
* @param {Object} options Object with configuration options. See {@link module:engine/model/treewalker~TreeWalker}.
* @returns {Iterable.}
*/
* getPositions( options = {} ) {
options.boundaries = this;
const treeWalker = new _treewalker__WEBPACK_IMPORTED_MODULE_1__["default"]( options );
yield treeWalker.position;
for ( const value of treeWalker ) {
yield value.nextPosition;
}
}
/**
* Returns a range that is a result of transforming this range by given `operation`.
*
* **Note:** transformation may break one range into multiple ranges (for example, when a part of the range is
* moved to a different part of document tree). For this reason, an array is returned by this method and it
* may contain one or more `Range` instances.
*
* @param {module:engine/model/operation/operation~Operation} operation Operation to transform range by.
* @returns {Array.} Range which is the result of transformation.
*/
getTransformedByOperation( operation ) {
switch ( operation.type ) {
case 'insert':
return this._getTransformedByInsertOperation( operation );
case 'move':
case 'remove':
case 'reinsert':
return this._getTransformedByMoveOperation( operation );
case 'split':
return [ this._getTransformedBySplitOperation( operation ) ];
case 'merge':
return [ this._getTransformedByMergeOperation( operation ) ];
}
return [ new Range( this.start, this.end ) ];
}
/**
* Returns a range that is a result of transforming this range by multiple `operations`.
*
* @see ~Range#getTransformedByOperation
* @param {Iterable.} operations Operations to transform the range by.
* @returns {Array.} Range which is the result of transformation.
*/
getTransformedByOperations( operations ) {
const ranges = [ new Range( this.start, this.end ) ];
for ( const operation of operations ) {
for ( let i = 0; i < ranges.length; i++ ) {
const result = ranges[ i ].getTransformedByOperation( operation );
ranges.splice( i, 1, ...result );
i += result.length - 1;
}
}
// It may happen that a range is split into two, and then the part of second "piece" is moved into first
// "piece". In this case we will have incorrect third range, which should not be included in the result --
// because it is already included in the first "piece". In this loop we are looking for all such ranges that
// are inside other ranges and we simply remove them.
for ( let i = 0; i < ranges.length; i++ ) {
const range = ranges[ i ];
for ( let j = i + 1; j < ranges.length; j++ ) {
const next = ranges[ j ];
if ( range.containsRange( next ) || next.containsRange( range ) || range.isEqual( next ) ) {
ranges.splice( j, 1 );
}
}
}
return ranges;
}
/**
* Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
* which is a common ancestor of the range's both ends (in which the entire range is contained).
*
* @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
*/
getCommonAncestor() {
return this.start.getCommonAncestor( this.end );
}
/**
* Returns an {@link module:engine/model/element~Element Element} contained by the range.
* The element will be returned when it is the **only** node within the range and **fully–contained**
* at the same time.
*
* @returns {module:engine/model/element~Element|null}
*/
getContainedElement() {
if ( this.isCollapsed ) {
return null;
}
const nodeAfterStart = this.start.nodeAfter;
const nodeBeforeEnd = this.end.nodeBefore;
if ( nodeAfterStart && nodeAfterStart.is( 'element' ) && nodeAfterStart === nodeBeforeEnd ) {
return nodeAfterStart;
}
return null;
}
/**
* Converts `Range` to plain object and returns it.
*
* @returns {Object} `Node` converted to plain object.
*/
toJSON() {
return {
start: this.start.toJSON(),
end: this.end.toJSON()
};
}
/**
* Returns a new range that is equal to current range.
*
* @returns {module:engine/model/range~Range}
*/
clone() {
return new this.constructor( this.start, this.end );
}
/**
* Returns a result of transforming a copy of this range by insert operation.
*
* One or more ranges may be returned as a result of this transformation.
*
* @protected
* @param {module:engine/model/operation/insertoperation~InsertOperation} operation
* @returns {Array.}
*/
_getTransformedByInsertOperation( operation, spread = false ) {
return this._getTransformedByInsertion( operation.position, operation.howMany, spread );
}
/**
* Returns a result of transforming a copy of this range by move operation.
*
* One or more ranges may be returned as a result of this transformation.
*
* @protected
* @param {module:engine/model/operation/moveoperation~MoveOperation} operation
* @returns {Array.}
*/
_getTransformedByMoveOperation( operation, spread = false ) {
const sourcePosition = operation.sourcePosition;
const howMany = operation.howMany;
const targetPosition = operation.targetPosition;
return this._getTransformedByMove( sourcePosition, targetPosition, howMany, spread );
}
/**
* Returns a result of transforming a copy of this range by split operation.
*
* Always one range is returned. The transformation is done in a way to not break the range.
*
* @protected
* @param {module:engine/model/operation/splitoperation~SplitOperation} operation
* @returns {module:engine/model/range~Range}
*/
_getTransformedBySplitOperation( operation ) {
const start = this.start._getTransformedBySplitOperation( operation );
let end = this.end._getTransformedBySplitOperation( operation );
if ( this.end.isEqual( operation.insertionPosition ) ) {
end = this.end.getShiftedBy( 1 );
}
// Below may happen when range contains graveyard element used by split operation.
if ( start.root != end.root ) {
// End position was next to the moved graveyard element and was moved with it.
// Fix it by using old `end` which has proper `root`.
end = this.end.getShiftedBy( -1 );
}
return new Range( start, end );
}
/**
* Returns a result of transforming a copy of this range by merge operation.
*
* Always one range is returned. The transformation is done in a way to not break the range.
*
* @protected
* @param {module:engine/model/operation/mergeoperation~MergeOperation} operation
* @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.
// F[o{o
B}ar
Xy]z
//
// Fo{o
B}ar
z
// F{
B}ar
z
// F{
}z
// F{}z
//
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 );
if ( start.root != end.root ) {
// This happens when the end position was next to the merged (deleted) element.
// Then, the end position was moved to the graveyard root. In this case we need to fix
// the range cause its boundaries would be in different roots.
end = this.end.getShiftedBy( -1 );
}
if ( start.isAfter( end ) ) {
// This happens in three following cases:
//
// Case 1: Merge operation source position is before the target position (due to some transformations, OT, etc.)
// This means that start can be moved before the end of the range.
//
// Before: a{a
b}b
cc
// Merge: b}b
cca{a
// Fix: {b}b
ccaa
//
// Case 2: Range start is before merged node but not directly.
// Result should include all nodes that were in the original range.
//
// Before: aa
{cc
b}b
// Merge: aab}b
{cc
// Fix: aa{bb
cc
}
//
// The range is expanded by an additional `b` letter but it is better than dropping the whole `cc` paragraph.
//
// Case 3: Range start is directly before merged node.
// Resulting range should include only nodes from the merged element:
//
// Before: aa
{b}b
cc
// Merge: aab}b
{cc
// Fix: aa{b}b
cc
//
if ( operation.sourcePosition.isBefore( operation.targetPosition ) ) {
// Case 1.
start = _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( end );
start.offset = 0;
} else {
if ( !operation.deletionPosition.isEqual( start ) ) {
// Case 2.
end = operation.deletionPosition;
}
// In both case 2 and 3 start is at the end of the merge-to element.
start = operation.targetPosition;
}
return new Range( start, end );
}
return new Range( start, end );
}
/**
* Returns an array containing one or two {@link ~Range ranges} that are a result of transforming this
* {@link ~Range range} by inserting `howMany` nodes at `insertPosition`. Two {@link ~Range ranges} are
* returned if the insertion was inside this {@link ~Range range} and `spread` is set to `true`.
*
* Examples:
*
* let range = model.createRange(
* model.createPositionFromPath( root, [ 2, 7 ] ),
* model.createPositionFromPath( root, [ 4, 0, 1 ] )
* );
* let transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 1 ] ), 2 );
* // transformed array has one range from [ 4, 7 ] to [ 6, 0, 1 ]
*
* transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 4, 0, 0 ] ), 4 );
* // transformed array has one range from [ 2, 7 ] to [ 4, 0, 5 ]
*
* transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 3, 2 ] ), 4 );
* // transformed array has one range, which is equal to original range
*
* transformed = range._getTransformedByInsertion( model.createPositionFromPath( root, [ 3, 2 ] ), 4, true );
* // transformed array has two ranges: from [ 2, 7 ] to [ 3, 2 ] and from [ 3, 6 ] to [ 4, 0, 1 ]
*
* @protected
* @param {module:engine/model/position~Position} insertPosition Position where nodes are inserted.
* @param {Number} howMany How many nodes are inserted.
* @param {Boolean} [spread] Flag indicating whether this {~Range range} should be spread if insertion
* was inside the range. Defaults to `false`.
* @returns {Array.} Result of the transformation.
*/
_getTransformedByInsertion( insertPosition, howMany, spread = false ) {
if ( spread && this.containsPosition( insertPosition ) ) {
// Range has to be spread. The first part is from original start to the spread point.
// The other part is from spread point to the original end, but transformed by
// insertion to reflect insertion changes.
return [
new Range( this.start, insertPosition ),
new Range(
insertPosition.getShiftedBy( howMany ),
this.end._getTransformedByInsertion( insertPosition, howMany )
)
];
} else {
const range = new Range( this.start, this.end );
range.start = range.start._getTransformedByInsertion( insertPosition, howMany );
range.end = range.end._getTransformedByInsertion( insertPosition, howMany );
return [ range ];
}
}
/**
* Returns an array containing {@link ~Range ranges} that are a result of transforming this
* {@link ~Range range} by moving `howMany` nodes from `sourcePosition` to `targetPosition`.
*
* @protected
* @param {module:engine/model/position~Position} sourcePosition Position from which nodes are moved.
* @param {module:engine/model/position~Position} targetPosition Position to where nodes are moved.
* @param {Number} howMany How many nodes are moved.
* @param {Boolean} [spread=false] Whether the range should be spread if the move points inside the range.
* @returns {Array.} Result of the transformation.
*/
_getTransformedByMove( sourcePosition, targetPosition, howMany, spread = false ) {
// Special case for transforming a collapsed range. Just transform it like a position.
if ( this.isCollapsed ) {
const newPos = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany );
return [ new Range( newPos ) ];
}
// Special case for transformation when a part of the range is moved towards the range.
//
// Examples:
//
// e]f
--> c[d
e]f
// e[f
--> e[f
a]b
//
// Without this special condition, the default algorithm leaves an "artifact" range from one of `differenceSet` parts:
//
// e]f
--> }c[d
e]f
//
// This special case is applied only if the range is to be kept together (not spread).
const moveRange = Range._createFromPositionAndShift( sourcePosition, howMany );
const insertPosition = targetPosition._getTransformedByDeletion( sourcePosition, howMany );
if ( this.containsPosition( targetPosition ) && !spread ) {
if ( moveRange.containsPosition( this.start ) || moveRange.containsPosition( this.end ) ) {
const start = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany );
const end = this.end._getTransformedByMove( sourcePosition, targetPosition, howMany );
return [ new Range( start, end ) ];
}
}
// Default algorithm.
let result;
const differenceSet = this.getDifference( moveRange );
let difference = null;
const common = this.getIntersection( moveRange );
if ( differenceSet.length == 1 ) {
// `moveRange` and this range may intersect but may be separate.
difference = new Range(
differenceSet[ 0 ].start._getTransformedByDeletion( sourcePosition, howMany ),
differenceSet[ 0 ].end._getTransformedByDeletion( sourcePosition, howMany )
);
} else if ( differenceSet.length == 2 ) {
// `moveRange` is inside this range.
difference = new Range(
this.start,
this.end._getTransformedByDeletion( sourcePosition, howMany )
);
} // else, `moveRange` contains this range.
if ( difference ) {
result = difference._getTransformedByInsertion( insertPosition, howMany, common !== null || spread );
} else {
result = [];
}
if ( common ) {
const transformedCommon = new Range(
common.start._getCombined( moveRange.start, insertPosition ),
common.end._getCombined( moveRange.start, insertPosition )
);
if ( result.length == 2 ) {
result.splice( 1, 0, transformedCommon );
} else {
result.push( transformedCommon );
}
}
return result;
}
/**
* Returns a copy of this range that is transformed by deletion of `howMany` nodes from `deletePosition`.
*
* If the deleted range is intersecting with the transformed range, the transformed range will be shrank.
*
* If the deleted range contains transformed range, `null` will be returned.
*
* @protected
* @param {module:engine/model/position~Position} deletionPosition Position from which nodes are removed.
* @param {Number} howMany How many nodes are removed.
* @returns {module:engine/model/range~Range|null} Result of the transformation.
*/
_getTransformedByDeletion( deletePosition, howMany ) {
let newStart = this.start._getTransformedByDeletion( deletePosition, howMany );
let newEnd = this.end._getTransformedByDeletion( deletePosition, howMany );
if ( newStart == null && newEnd == null ) {
return null;
}
if ( newStart == null ) {
newStart = deletePosition;
}
if ( newEnd == null ) {
newEnd = deletePosition;
}
return new Range( newStart, newEnd );
}
/**
* Creates a new range, spreading from specified {@link module:engine/model/position~Position position} to a position moved by
* given `shift`. If `shift` is a negative value, shifted position is treated as the beginning of the range.
*
* @protected
* @param {module:engine/model/position~Position} position Beginning of the range.
* @param {Number} shift How long the range should be.
* @returns {module:engine/model/range~Range}
*/
static _createFromPositionAndShift( position, shift ) {
const start = position;
const end = position.getShiftedBy( shift );
return shift > 0 ? new this( start, end ) : new this( end, start );
}
/**
* Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
* that element and ends after the last child of that element.
*
* @protected
* @param {module:engine/model/element~Element} element Element which is a parent for the range.
* @returns {module:engine/model/range~Range}
*/
static _createIn( element ) {
return new this( _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( element, 0 ), _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( element, element.maxOffset ) );
}
/**
* Creates a range that starts before given {@link module:engine/model/item~Item model item} and ends after it.
*
* @protected
* @param {module:engine/model/item~Item} item
* @returns {module:engine/model/range~Range}
*/
static _createOn( item ) {
return this._createFromPositionAndShift( _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createBefore( item ), item.offsetSize );
}
/**
* Combines all ranges from the passed array into a one range. At least one range has to be passed.
* Passed ranges must not have common parts.
*
* The first range from the array is a reference range. If other ranges start or end on the exactly same position where
* the reference range, they get combined into one range.
*
* [ ][] [ ][ ][ ][ ][] [ ] // Passed ranges, shown sorted
* [ ] // The result of the function if the first range was a reference range.
* [ ] // The result of the function if the third-to-seventh range was a reference range.
* [ ] // The result of the function if the last range was a reference range.
*
* @param {Array.} ranges Ranges to combine.
* @returns {module:engine/model/range~Range} Combined range.
*/
static _createFromRanges( ranges ) {
if ( ranges.length === 0 ) {
/**
* At least one range has to be passed to
* {@link module:engine/model/range~Range._createFromRanges `Range._createFromRanges()`}.
*
* @error range-create-from-ranges-empty-array
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_2__["default"](
'range-create-from-ranges-empty-array',
null
);
} else if ( ranges.length == 1 ) {
return ranges[ 0 ].clone();
}
// 1. Set the first range in `ranges` array as a reference range.
// If we are going to return just a one range, one of the ranges need to be the reference one.
// Other ranges will be stuck to that range, if possible.
const ref = ranges[ 0 ];
// 2. Sort all the ranges so it's easier to process them.
ranges.sort( ( a, b ) => {
return a.start.isAfter( b.start ) ? 1 : -1;
} );
// 3. Check at which index the reference range is now.
const refIndex = ranges.indexOf( ref );
// 4. At this moment we don't need the original range.
// We are going to modify the result and we need to return a new instance of Range.
// We have to create a copy of the reference range.
const result = new this( ref.start, ref.end );
// 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
// Since ranges are sorted, start with the range with index that is closest to reference range index.
if ( refIndex > 0 ) {
for ( let i = refIndex - 1; true; i++ ) {
if ( ranges[ i ].end.isEqual( result.start ) ) {
result.start = _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( ranges[ i ].start );
} else {
// If ranges are not starting/ending at the same position there is no point in looking further.
break;
}
}
}
// 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
// Since ranges are sorted, start with the range with index that is closest to reference range index.
for ( let i = refIndex + 1; i < ranges.length; i++ ) {
if ( ranges[ i ].start.isEqual( result.end ) ) {
result.end = _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( ranges[ i ].end );
} else {
// If ranges are not starting/ending at the same position there is no point in looking further.
break;
}
}
return result;
}
/**
* Creates a `Range` instance from given plain object (i.e. parsed JSON string).
*
* @param {Object} json Plain object to be converted to `Range`.
* @param {module:engine/model/document~Document} doc Document object that will be range owner.
* @returns {module:engine/model/element~Element} `Range` instance created using given plain object.
*/
static fromJSON( json, doc ) {
return new this( _position__WEBPACK_IMPORTED_MODULE_0__["default"].fromJSON( json.start, doc ), _position__WEBPACK_IMPORTED_MODULE_0__["default"].fromJSON( json.end, doc ) );
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return `${ this.root } [ ${ this.start.path.join( ', ' ) } ] - [ ${ this.end.path.join( ', ' ) } ]`;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // log() {
// @if CK_DEBUG_ENGINE // console.log( 'ModelPosition: ' + this );
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/rootelement.js":
/*!**************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/rootelement.js ***!
\**************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return RootElement; });
/* harmony import */ var _element__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./element */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js");
/**
* @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 engine/model/rootelement
*/
/**
* Type of {@link module:engine/model/element~Element} that is a root of a model tree.
* @extends module:engine/model/element~Element
*/
class RootElement extends _element__WEBPACK_IMPORTED_MODULE_0__["default"] {
/**
* Creates root element.
*
* @param {module:engine/model/document~Document} document Document that is an owner of this root.
* @param {String} name Node name.
* @param {String} [rootName='main'] Unique root name used to identify this root
* element by {@link module:engine/model/document~Document}.
*/
constructor( document, name, rootName = 'main' ) {
super( name );
/**
* Document that is an owner of this root.
*
* @private
* @member {module:engine/model/document~Document}
*/
this._document = document;
/**
* Unique root name used to identify this root element by {@link module:engine/model/document~Document}.
*
* @readonly
* @member {String}
*/
this.rootName = rootName;
}
/**
* {@link module:engine/model/document~Document Document} that owns this root element.
*
* @readonly
* @type {module:engine/model/document~Document|null}
*/
get document() {
return this._document;
}
/**
* Checks whether this object is of the given.
*
* rootElement.is( 'rootElement' ); // -> true
* rootElement.is( 'element' ); // -> true
* rootElement.is( 'node' ); // -> true
* rootElement.is( 'model:rootElement' ); // -> true
* rootElement.is( 'model:element' ); // -> true
* rootElement.is( 'model:node' ); // -> true
*
* rootElement.is( 'view:element' ); // -> false
* rootElement.is( 'documentFragment' ); // -> false
*
* Assuming that the object being checked is an element, you can also check its
* {@link module:engine/model/element~Element#name name}:
*
* rootElement.is( 'rootElement', '$root' ); // -> same as above
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type Type to check.
* @param {String} [name] Element name.
* @returns {Boolean}
*/
is( type, name ) {
if ( !name ) {
return type === 'rootElement' || type === 'model:rootElement' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === 'element' || type === 'model:element' ||
type === 'node' || type === 'model:node';
}
return name === this.name && (
type === 'rootElement' || type === 'model:rootElement' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === 'element' || type === 'model:element'
);
}
/**
* Converts `RootElement` instance to `String` containing it's name.
*
* @returns {String} `RootElement` instance converted to `String`.
*/
toJSON() {
return this.rootName;
}
// @if CK_DEBUG_ENGINE // toString() {
// @if CK_DEBUG_ENGINE // return this.rootName;
// @if CK_DEBUG_ENGINE // }
// @if CK_DEBUG_ENGINE // log() {
// @if CK_DEBUG_ENGINE // console.log( 'ModelRootElement: ' + this );
// @if CK_DEBUG_ENGINE // }
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/schema.js":
/*!*********************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/schema.js ***!
\*********************************************************************/
/*! exports provided: default, SchemaContext */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Schema; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SchemaContext", function() { return SchemaContext; });
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_observablemixin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/observablemixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/observablemixin.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _element__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./element */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/element.js");
/* harmony import */ var _text__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./text */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/text.js");
/* harmony import */ var _treewalker__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./treewalker */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/treewalker.js");
/**
* @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 engine/model/schema
*/
/**
* The model's schema. It defines allowed and disallowed structures of nodes as well as nodes' attributes.
* The schema is usually defined by features and based on them the editing framework and features
* make decisions how to change and process the model.
*
* The instance of schema is available in {@link module:engine/model/model~Model#schema `editor.model.schema`}.
*
* Read more about the schema in:
*
* * {@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.
*
* @mixes module:utils/observablemixin~ObservableMixin
*/
class Schema {
/**
* Creates schema instance.
*/
constructor() {
this._sourceDefinitions = {};
/**
* A dictionary containing attribute properties.
*
* @private
* @member {Object.}
*/
this._attributeProperties = {};
this.decorate( 'checkChild' );
this.decorate( 'checkAttribute' );
this.on( 'checkAttribute', ( evt, args ) => {
args[ 0 ] = new SchemaContext( args[ 0 ] );
}, { priority: 'highest' } );
this.on( 'checkChild', ( evt, args ) => {
args[ 0 ] = new SchemaContext( args[ 0 ] );
args[ 1 ] = this.getDefinition( args[ 1 ] );
}, { priority: 'highest' } );
}
/**
* Registers schema item. Can only be called once for every item name.
*
* schema.register( 'paragraph', {
* inheritAllFrom: '$block'
* } );
*
* @param {String} itemName
* @param {module:engine/model/schema~SchemaItemDefinition} definition
*/
register( itemName, definition ) {
if ( this._sourceDefinitions[ itemName ] ) {
/**
* A single item cannot be registered twice in the schema.
*
* This situation may happen when:
*
* * Two or more plugins called {@link #register `register()`} with the same name. This will usually mean that
* there is a collision between plugins which try to use the same element in the model. Unfortunately,
* the only way to solve this is by modifying one of these plugins to use a unique model element name.
* * A single plugin was loaded twice. This happens when it is installed by npm/yarn in two versions
* and usually means one or more of the following issues:
* * a version mismatch (two of your dependencies require two different versions of this plugin),
* * incorrect imports (this plugin is somehow imported twice in a way which confuses webpack),
* * mess in `node_modules/` (`rm -rf node_modules/` may help).
*
* **Note:** Check the logged `itemName` to better understand which plugin was duplicated/conflicting.
*
* @param itemName The name of the model element that is being registered twice.
* @error schema-cannot-register-item-twice
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_0__["default"](
'schema-cannot-register-item-twice',
this,
{
itemName
}
);
}
this._sourceDefinitions[ itemName ] = [
Object.assign( {}, definition )
];
this._clearCache();
}
/**
* Extends a {@link #register registered} item's definition.
*
* Extending properties such as `allowIn` will add more items to the existing properties,
* while redefining properties such as `isBlock` will override the previously defined ones.
*
* schema.register( 'foo', {
* allowIn: '$root',
* isBlock: true;
* } );
* schema.extend( 'foo', {
* allowIn: 'blockQuote',
* isBlock: false
* } );
*
* schema.getDefinition( 'foo' );
* // {
* // allowIn: [ '$root', 'blockQuote' ],
* // isBlock: false
* // }
*
* @param {String} itemName
* @param {module:engine/model/schema~SchemaItemDefinition} definition
*/
extend( itemName, definition ) {
if ( !this._sourceDefinitions[ itemName ] ) {
/**
* Cannot extend an item which was not registered yet.
*
* This error happens when a plugin tries to extend the schema definition of an item which was not
* {@link #register registered} yet.
*
* @param itemName The name of the model element which is being extended.
* @error schema-cannot-extend-missing-item
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_0__["default"]( 'schema-cannot-extend-missing-item', this, {
itemName
} );
}
this._sourceDefinitions[ itemName ].push( Object.assign( {}, definition ) );
this._clearCache();
}
/**
* Returns data of all registered items.
*
* This method should normally be used for reflection purposes (e.g. defining a clone of a certain element,
* checking a list of all block elements, etc).
* Use specific methods (such as {@link #checkChild `checkChild()`} or {@link #isLimit `isLimit()`})
* in other cases.
*
* @returns {Object.}
*/
getDefinitions() {
if ( !this._compiledDefinitions ) {
this._compile();
}
return this._compiledDefinitions;
}
/**
* Returns a definition of the given item or `undefined` if an item is not registered.
*
* This method should normally be used for reflection purposes (e.g. defining a clone of a certain element,
* checking a list of all block elements, etc).
* Use specific methods (such as {@link #checkChild `checkChild()`} or {@link #isLimit `isLimit()`})
* in other cases.
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
* @returns {module:engine/model/schema~SchemaCompiledItemDefinition}
*/
getDefinition( item ) {
let itemName;
if ( typeof item == 'string' ) {
itemName = item;
} else if ( item.is && ( item.is( '$text' ) || item.is( '$textProxy' ) ) ) {
itemName = '$text';
}
// Element or module:engine/model/schema~SchemaContextItem.
else {
itemName = item.name;
}
return this.getDefinitions()[ itemName ];
}
/**
* Returns `true` if the given item is registered in the schema.
*
* schema.isRegistered( 'paragraph' ); // -> true
* schema.isRegistered( editor.model.document.getRoot() ); // -> true
* schema.isRegistered( 'foo' ); // -> false
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
*/
isRegistered( item ) {
return !!this.getDefinition( item );
}
/**
* Returns `true` if the given item is defined to be
* a block by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isBlock` property.
*
* schema.isBlock( 'paragraph' ); // -> true
* schema.isBlock( '$root' ); // -> false
*
* const paragraphElement = writer.createElement( 'paragraph' );
* schema.isBlock( paragraphElement ); // -> true
*
* See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive
* guide for more details.
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
*/
isBlock( item ) {
const def = this.getDefinition( item );
return !!( def && def.isBlock );
}
/**
* Returns `true` if the given item should be treated as a limit element.
*
* 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
* was set to `true`.
*
* schema.isLimit( 'paragraph' ); // -> false
* schema.isLimit( '$root' ); // -> true
* schema.isLimit( editor.model.document.getRoot() ); // -> true
* schema.isLimit( 'image' ); // -> true
*
* See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive
* guide for more details.
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
*/
isLimit( item ) {
const def = this.getDefinition( item );
if ( !def ) {
return false;
}
return !!( def.isLimit || def.isObject );
}
/**
* Returns `true` if the given item should be treated as an object element.
*
* 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
* was set to `true`.
*
* schema.isObject( 'paragraph' ); // -> false
* schema.isObject( 'image' ); // -> true
*
* const imageElement = writer.createElement( 'image' );
* schema.isObject( imageElement ); // -> true
*
* See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive
* guide for more details.
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
*/
isObject( item ) {
const def = this.getDefinition( item );
if ( !def ) {
return false;
}
// Note: Check out the implementation of #isLimit(), #isSelectable(), and #isContent()
// to understand why these three constitute an object.
return !!( def.isObject || ( def.isLimit && def.isSelectable && def.isContent ) );
}
/**
* Returns `true` if the given item is defined to be
* an inline element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isInline` property.
*
* schema.isInline( 'paragraph' ); // -> false
* schema.isInline( 'softBreak' ); // -> true
*
* const text = writer.createText( 'foo' );
* schema.isInline( text ); // -> true
*
* See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive
* guide for more details.
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
*/
isInline( item ) {
const def = this.getDefinition( item );
return !!( def && def.isInline );
}
/**
* Returns `true` if the given item is defined to be
* a selectable element by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isSelectable` property.
*
* schema.isSelectable( 'paragraph' ); // -> false
* schema.isSelectable( 'heading1' ); // -> false
* schema.isSelectable( 'image' ); // -> true
* schema.isSelectable( 'tableCell' ); // -> true
*
* const text = writer.createText( 'foo' );
* schema.isSelectable( text ); // -> false
*
* See the {@glink framework/guides/deep-dive/schema#selectable-elements Selectable elements} section of the Schema deep dive}
* guide for more details.
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
*/
isSelectable( item ) {
const def = this.getDefinition( item );
if ( !def ) {
return false;
}
return !!( def.isSelectable || def.isObject );
}
/**
* Returns `true` if the given item is defined to be
* a content by the {@link module:engine/model/schema~SchemaItemDefinition}'s `isContent` property.
*
* schema.isContent( 'paragraph' ); // -> false
* schema.isContent( 'heading1' ); // -> false
* schema.isContent( 'image' ); // -> true
* schema.isContent( 'horizontalLine' ); // -> true
*
* const text = writer.createText( 'foo' );
* schema.isContent( text ); // -> true
*
* See the {@glink framework/guides/deep-dive/schema#content-elements Content elements} section of the Schema deep dive}
* guide for more details.
*
* @param {module:engine/model/item~Item|module:engine/model/schema~SchemaContextItem|String} item
*/
isContent( item ) {
const def = this.getDefinition( item );
if ( !def ) {
return false;
}
return !!( def.isContent || def.isObject );
}
/**
* Checks whether the given node (`child`) can be a child of the given context.
*
* schema.checkChild( model.document.getRoot(), paragraph ); // -> false
*
* schema.register( 'paragraph', {
* allowIn: '$root'
* } );
* schema.checkChild( model.document.getRoot(), paragraph ); // -> true
*
* Note: When verifying whether the given node can be a child of the given context, the
* schema also verifies the entire context — from its root to its last element. Therefore, it is possible
* for `checkChild()` to return `false` even though the context's last element can contain the checked child.
* It happens if one of the context's elements does not allow its child.
*
* @fires checkChild
* @param {module:engine/model/schema~SchemaContextDefinition} context The context in which the child will be checked.
* @param {module:engine/model/node~Node|String} def The child to check.
*/
checkChild( context, def ) {
// Note: context and child are already normalized here to a SchemaContext and SchemaCompiledItemDefinition.
if ( !def ) {
return false;
}
return this._checkContextMatch( def, context );
}
/**
* Checks whether the given attribute can be applied in the given context (on the last
* item of the context).
*
* schema.checkAttribute( textNode, 'bold' ); // -> false
*
* schema.extend( '$text', {
* allowAttributes: 'bold'
* } );
* schema.checkAttribute( textNode, 'bold' ); // -> true
*
* @fires checkAttribute
* @param {module:engine/model/schema~SchemaContextDefinition} context The context in which the attribute will be checked.
* @param {String} attributeName
*/
checkAttribute( context, attributeName ) {
const def = this.getDefinition( context.last );
if ( !def ) {
return false;
}
return def.allowAttributes.includes( attributeName );
}
/**
* Checks whether the given element (`elementToMerge`) can be merged with the specified base element (`positionOrBaseElement`).
*
* In other words — whether `elementToMerge`'s children {@link #checkChild are allowed} in the `positionOrBaseElement`.
*
* This check ensures that elements merged with {@link module:engine/model/writer~Writer#merge `Writer#merge()`}
* will be valid.
*
* Instead of elements, you can pass the instance of the {@link module:engine/model/position~Position} class as the
* `positionOrBaseElement`. It means that the elements before and after the position will be checked whether they can be merged.
*
* @param {module:engine/model/position~Position|module:engine/model/element~Element} positionOrBaseElement The position or base
* element to which the `elementToMerge` will be merged.
* @param {module:engine/model/element~Element} elementToMerge The element to merge. Required if `positionOrBaseElement` is an element.
* @returns {Boolean}
*/
checkMerge( positionOrBaseElement, elementToMerge = null ) {
if ( positionOrBaseElement instanceof _position__WEBPACK_IMPORTED_MODULE_4__["default"] ) {
const nodeBefore = positionOrBaseElement.nodeBefore;
const nodeAfter = positionOrBaseElement.nodeAfter;
if ( !( nodeBefore instanceof _element__WEBPACK_IMPORTED_MODULE_5__["default"] ) ) {
/**
* The node before the merge position must be an element.
*
* @error schema-check-merge-no-element-before
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_0__["default"](
'schema-check-merge-no-element-before',
this
);
}
if ( !( nodeAfter instanceof _element__WEBPACK_IMPORTED_MODULE_5__["default"] ) ) {
/**
* The node after the merge position must be an element.
*
* @error schema-check-merge-no-element-after
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_0__["default"](
'schema-check-merge-no-element-after',
this
);
}
return this.checkMerge( nodeBefore, nodeAfter );
}
for ( const child of elementToMerge.getChildren() ) {
if ( !this.checkChild( positionOrBaseElement, child ) ) {
return false;
}
}
return true;
}
/**
* Allows registering a callback to the {@link #checkChild} method calls.
*
* Callbacks allow you to implement rules which are not otherwise possible to achieve
* by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
* For example, by using this method you can disallow elements in specific contexts.
*
* This method is a shorthand for using the {@link #event:checkChild} event. For even better control,
* you can use that event instead.
*
* Example:
*
* // Disallow heading1 directly inside a blockQuote.
* schema.addChildCheck( ( context, childDefinition ) => {
* if ( context.endsWith( 'blockQuote' ) && childDefinition.name == 'heading1' ) {
* return false;
* }
* } );
*
* Which translates to:
*
* schema.on( 'checkChild', ( evt, args ) => {
* const context = args[ 0 ];
* const childDefinition = args[ 1 ];
*
* if ( context.endsWith( 'blockQuote' ) && childDefinition && childDefinition.name == 'heading1' ) {
* // Prevent next listeners from being called.
* evt.stop();
* // Set the checkChild()'s return value.
* evt.return = false;
* }
* }, { priority: 'high' } );
*
* @param {Function} callback The callback to be called. It is called with two parameters:
* {@link module:engine/model/schema~SchemaContext} (context) instance and
* {@link module:engine/model/schema~SchemaCompiledItemDefinition} (child-to-check definition).
* The callback may return `true/false` to override `checkChild()`'s return value. If it does not return
* a boolean value, the default algorithm (or other callbacks) will define `checkChild()`'s return value.
*/
addChildCheck( callback ) {
this.on( 'checkChild', ( evt, [ ctx, childDef ] ) => {
// checkChild() was called with a non-registered child.
// In 99% cases such check should return false, so not to overcomplicate all callbacks
// don't even execute them.
if ( !childDef ) {
return;
}
const retValue = callback( ctx, childDef );
if ( typeof retValue == 'boolean' ) {
evt.stop();
evt.return = retValue;
}
}, { priority: 'high' } );
}
/**
* Allows registering a callback to the {@link #checkAttribute} method calls.
*
* Callbacks allow you to implement rules which are not otherwise possible to achieve
* by using the declarative API of {@link module:engine/model/schema~SchemaItemDefinition}.
* For example, by using this method you can disallow attribute if node to which it is applied
* is contained within some other element (e.g. you want to disallow `bold` on `$text` within `heading1`).
*
* This method is a shorthand for using the {@link #event:checkAttribute} event. For even better control,
* you can use that event instead.
*
* Example:
*
* // Disallow bold on $text inside heading1.
* schema.addAttributeCheck( ( context, attributeName ) => {
* if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
* return false;
* }
* } );
*
* Which translates to:
*
* schema.on( 'checkAttribute', ( evt, args ) => {
* const context = args[ 0 ];
* const attributeName = args[ 1 ];
*
* if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
* // Prevent next listeners from being called.
* evt.stop();
* // Set the checkAttribute()'s return value.
* evt.return = false;
* }
* }, { priority: 'high' } );
*
* @param {Function} callback The callback to be called. It is called with two parameters:
* {@link module:engine/model/schema~SchemaContext} (context) instance and attribute name.
* The callback may return `true/false` to override `checkAttribute()`'s return value. If it does not return
* a boolean value, the default algorithm (or other callbacks) will define `checkAttribute()`'s return value.
*/
addAttributeCheck( callback ) {
this.on( 'checkAttribute', ( evt, [ ctx, attributeName ] ) => {
const retValue = callback( ctx, attributeName );
if ( typeof retValue == 'boolean' ) {
evt.stop();
evt.return = retValue;
}
}, { priority: 'high' } );
}
/**
* This method allows assigning additional metadata to the model attributes. For example,
* {@link module:engine/model/schema~AttributeProperties `AttributeProperties#isFormatting` property} is
* used to mark formatting attributes (like `bold` or `italic`).
*
* // Mark bold as a formatting attribute.
* schema.setAttributeProperties( 'bold', {
* isFormatting: true
* } );
*
* // Override code not to be considered a formatting markup.
* schema.setAttributeProperties( 'code', {
* isFormatting: false
* } );
*
* Properties are not limited to members defined in the
* {@link module:engine/model/schema~AttributeProperties `AttributeProperties` type} and you can also use custom properties:
*
* schema.setAttributeProperties( 'blockQuote', {
* customProperty: 'value'
* } );
*
* Subsequent calls with the same attribute will extend its custom properties:
*
* schema.setAttributeProperties( 'blockQuote', {
* one: 1
* } );
*
* schema.setAttributeProperties( 'blockQuote', {
* two: 2
* } );
*
* console.log( schema.getAttributeProperties( 'blockQuote' ) );
* // Logs: { one: 1, two: 2 }
*
* @param {String} attributeName A name of the attribute to receive the properties.
* @param {module:engine/model/schema~AttributeProperties} properties A dictionary of properties.
*/
setAttributeProperties( attributeName, properties ) {
this._attributeProperties[ attributeName ] = Object.assign( this.getAttributeProperties( attributeName ), properties );
}
/**
* Returns properties associated with a given model attribute. See {@link #setAttributeProperties `setAttributeProperties()`}.
*
* @param {String} attributeName A name of the attribute.
* @returns {module:engine/model/schema~AttributeProperties}
*/
getAttributeProperties( attributeName ) {
return this._attributeProperties[ attributeName ] || {};
}
/**
* Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
* selection/range/position or the root otherwise.
*
* @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
* module:engine/model/range~Range|module:engine/model/position~Position} selectionOrRangeOrPosition
* The selection/range/position to check.
* @returns {module:engine/model/element~Element} The lowest limit element containing
* the entire `selectionOrRangeOrPosition`.
*/
getLimitElement( selectionOrRangeOrPosition ) {
let element;
if ( selectionOrRangeOrPosition instanceof _position__WEBPACK_IMPORTED_MODULE_4__["default"] ) {
element = selectionOrRangeOrPosition.parent;
} else {
const ranges = selectionOrRangeOrPosition instanceof _range__WEBPACK_IMPORTED_MODULE_3__["default"] ?
[ selectionOrRangeOrPosition ] :
Array.from( selectionOrRangeOrPosition.getRanges() );
// Find the common ancestor for all selection's ranges.
element = ranges
.reduce( ( element, range ) => {
const rangeCommonAncestor = range.getCommonAncestor();
if ( !element ) {
return rangeCommonAncestor;
}
return element.getCommonAncestor( rangeCommonAncestor, { includeSelf: true } );
}, null );
}
while ( !this.isLimit( element ) ) {
if ( element.parent ) {
element = element.parent;
} else {
break;
}
}
return element;
}
/**
* Checks whether the attribute is allowed in selection:
*
* * if the selection is not collapsed, then checks if the attribute is allowed on any of nodes in that range,
* * if the selection is collapsed, then checks if on the selection position there's a text with the
* specified attribute allowed.
*
* @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
* Selection which will be checked.
* @param {String} attribute The name of the attribute to check.
* @returns {Boolean}
*/
checkAttributeInSelection( selection, attribute ) {
if ( selection.isCollapsed ) {
const firstPosition = selection.getFirstPosition();
const context = [
...firstPosition.getAncestors(),
new _text__WEBPACK_IMPORTED_MODULE_6__["default"]( '', selection.getAttributes() )
];
// Check whether schema allows for a text with the attribute in the selection.
return this.checkAttribute( context, attribute );
} else {
const ranges = selection.getRanges();
// For all ranges, check nodes in them until you find a node that is allowed to have the attribute.
for ( const range of ranges ) {
for ( const value of range ) {
if ( this.checkAttribute( value.item, attribute ) ) {
// If we found a node that is allowed to have the attribute, return true.
return true;
}
}
}
}
// If we haven't found such node, return false.
return false;
}
/**
* Transforms the given set of ranges into a set of ranges where the given attribute is allowed (and can be applied).
*
* @param {Array.} ranges Ranges to be validated.
* @param {String} attribute The name of the attribute to check.
* @returns {Iterable.} Ranges in which the attribute is allowed.
*/
* getValidRanges( ranges, attribute ) {
ranges = convertToMinimalFlatRanges( ranges );
for ( const range of ranges ) {
yield* this._getValidRangesForRange( range, attribute );
}
}
/**
* Basing on given `position`, finds and returns a {@link module:engine/model/range~Range range} which is
* nearest to that `position` and is a correct range for selection.
*
* The correct selection range might be collapsed when it is located in a position where the text node can be placed.
* Non-collapsed range is returned when selection can be placed around element marked as an "object" in
* the {@link module:engine/model/schema~Schema schema}.
*
* Direction of searching for the nearest correct selection range can be specified as:
*
* * `both` - searching will be performed in both ways,
* * `forward` - searching will be performed only forward,
* * `backward` - searching will be performed only backward.
*
* When valid selection range cannot be found, `null` is returned.
*
* @param {module:engine/model/position~Position} position Reference position where new selection range should be looked for.
* @param {'both'|'forward'|'backward'} [direction='both'] Search direction.
* @returns {module:engine/model/range~Range|null} Nearest selection range or `null` if one cannot be found.
*/
getNearestSelectionRange( position, direction = 'both' ) {
// Return collapsed range if provided position is valid.
if ( this.checkChild( position, '$text' ) ) {
return new _range__WEBPACK_IMPORTED_MODULE_3__["default"]( position );
}
let backwardWalker, forwardWalker;
// Never leave a limit element.
const limitElement = position.getAncestors().reverse().find( item => this.isLimit( item ) ) || position.root;
if ( direction == 'both' || direction == 'backward' ) {
backwardWalker = new _treewalker__WEBPACK_IMPORTED_MODULE_7__["default"]( {
boundaries: _range__WEBPACK_IMPORTED_MODULE_3__["default"]._createIn( limitElement ),
startPosition: position,
direction: 'backward'
} );
}
if ( direction == 'both' || direction == 'forward' ) {
forwardWalker = new _treewalker__WEBPACK_IMPORTED_MODULE_7__["default"]( {
boundaries: _range__WEBPACK_IMPORTED_MODULE_3__["default"]._createIn( limitElement ),
startPosition: position
} );
}
for ( const data of combineWalkers( backwardWalker, forwardWalker ) ) {
const type = ( data.walker == backwardWalker ? 'elementEnd' : 'elementStart' );
const value = data.value;
if ( value.type == type && this.isObject( value.item ) ) {
return _range__WEBPACK_IMPORTED_MODULE_3__["default"]._createOn( value.item );
}
if ( this.checkChild( value.nextPosition, '$text' ) ) {
return new _range__WEBPACK_IMPORTED_MODULE_3__["default"]( value.nextPosition );
}
}
return null;
}
/**
* 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 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 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.
*/
findAllowedParent( position, node ) {
let parent = position.parent;
while ( parent ) {
if ( this.checkChild( parent, node ) ) {
return parent;
}
// Do not split limit elements.
if ( this.isLimit( parent ) ) {
return null;
}
parent = parent.parent;
}
return null;
}
/**
* Removes attributes disallowed by the schema.
*
* @param {Iterable.} nodes Nodes that will be filtered.
* @param {module:engine/model/writer~Writer} writer
*/
removeDisallowedAttributes( nodes, writer ) {
for ( const node of nodes ) {
// When node is a `Text` it has no children, so just filter it out.
if ( node.is( '$text' ) ) {
removeDisallowedAttributeFromNode( this, node, writer );
}
// In a case of `Element` iterates through positions between nodes inside this element
// and filter out node before the current position, or position parent when position
// is at start of an element. Using positions prevent from omitting merged nodes
// see https://github.com/ckeditor/ckeditor5-engine/issues/1789.
else {
const rangeInNode = _range__WEBPACK_IMPORTED_MODULE_3__["default"]._createIn( node );
const positionsInRange = rangeInNode.getPositions();
for ( const position of positionsInRange ) {
const item = position.nodeBefore || position.parent;
removeDisallowedAttributeFromNode( this, item, writer );
}
}
}
}
/**
* Creates an instance of the schema context.
*
* @param {module:engine/model/schema~SchemaContextDefinition} context
* @returns {module:engine/model/schema~SchemaContext}
*/
createContext( context ) {
return new SchemaContext( context );
}
/**
* @private
*/
_clearCache() {
this._compiledDefinitions = null;
}
/**
* @private
*/
_compile() {
const compiledDefinitions = {};
const sourceRules = this._sourceDefinitions;
const itemNames = Object.keys( sourceRules );
for ( const itemName of itemNames ) {
compiledDefinitions[ itemName ] = compileBaseItemRule( sourceRules[ itemName ], itemName );
}
for ( const itemName of itemNames ) {
compileAllowContentOf( compiledDefinitions, itemName );
}
for ( const itemName of itemNames ) {
compileAllowWhere( compiledDefinitions, itemName );
}
for ( const itemName of itemNames ) {
compileAllowAttributesOf( compiledDefinitions, itemName );
compileInheritPropertiesFrom( compiledDefinitions, itemName );
}
for ( const itemName of itemNames ) {
cleanUpAllowIn( compiledDefinitions, itemName );
cleanUpAllowAttributes( compiledDefinitions, itemName );
}
this._compiledDefinitions = compiledDefinitions;
}
/**
* @private
* @param {module:engine/model/schema~SchemaCompiledItemDefinition} def
* @param {module:engine/model/schema~SchemaContext} context
* @param {Number} contextItemIndex
*/
_checkContextMatch( def, context, contextItemIndex = context.length - 1 ) {
const contextItem = context.getItem( contextItemIndex );
if ( def.allowIn.includes( contextItem.name ) ) {
if ( contextItemIndex == 0 ) {
return true;
} else {
const parentRule = this.getDefinition( contextItem );
return this._checkContextMatch( parentRule, context, contextItemIndex - 1 );
}
} else {
return false;
}
}
/**
* Takes a flat range and an attribute name. Traverses the range recursively and deeply to find and return all ranges
* inside the given range on which the attribute can be applied.
*
* This is a helper function for {@link ~Schema#getValidRanges}.
*
* @private
* @param {module:engine/model/range~Range} range The range to process.
* @param {String} attribute The name of the attribute to check.
* @returns {Iterable.} Ranges in which the attribute is allowed.
*/
* _getValidRangesForRange( range, attribute ) {
let start = range.start;
let end = range.start;
for ( const item of range.getItems( { shallow: true } ) ) {
if ( item.is( 'element' ) ) {
yield* this._getValidRangesForRange( _range__WEBPACK_IMPORTED_MODULE_3__["default"]._createIn( item ), attribute );
}
if ( !this.checkAttribute( item, attribute ) ) {
if ( !start.isEqual( end ) ) {
yield new _range__WEBPACK_IMPORTED_MODULE_3__["default"]( start, end );
}
start = _position__WEBPACK_IMPORTED_MODULE_4__["default"]._createAfter( item );
}
end = _position__WEBPACK_IMPORTED_MODULE_4__["default"]._createAfter( item );
}
if ( !start.isEqual( end ) ) {
yield new _range__WEBPACK_IMPORTED_MODULE_3__["default"]( start, end );
}
}
}
Object(_ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_2__["default"])( Schema, _ckeditor_ckeditor5_utils_src_observablemixin__WEBPACK_IMPORTED_MODULE_1__["default"] );
/**
* Event fired when the {@link #checkChild} method is called. It allows plugging in
* additional behavior, for example implementing rules which cannot be defined using the declarative
* {@link module:engine/model/schema~SchemaItemDefinition} interface.
*
* **Note:** The {@link #addChildCheck} method is a more handy way to register callbacks. Internally,
* it registers a listener to this event but comes with a simpler API and it is the recommended choice
* in most of the cases.
*
* The {@link #checkChild} 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 various ways, but the most important use case is overriding standard behavior of the
* `checkChild()` method. Let's see a typical listener template:
*
* schema.on( 'checkChild', ( evt, args ) => {
* const context = args[ 0 ];
* const childDefinition = args[ 1 ];
* }, { priority: 'high' } );
*
* The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
* parameter contains arguments passed to `checkChild( context, child )`. However, the `context` parameter is already
* normalized to a {@link module:engine/model/schema~SchemaContext} instance and `child` to a
* {@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()`.
*
* **Note:** `childDefinition` may be `undefined` if `checkChild()` was called with a non-registered element.
*
* So, in order to implement a rule "disallow `heading1` in `blockQuote`", you can add such a listener:
*
* schema.on( 'checkChild', ( evt, args ) => {
* const context = args[ 0 ];
* const childDefinition = args[ 1 ];
*
* if ( context.endsWith( 'blockQuote' ) && childDefinition && childDefinition.name == 'heading1' ) {
* // Prevent next listeners from being called.
* evt.stop();
* // Set the checkChild()'s return value.
* evt.return = false;
* }
* }, { priority: 'high' } );
*
* 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:
*
* schema.on( 'checkChild', ( evt, args ) => {
* const context = args[ 0 ];
* const childDefinition = args[ 1 ];
*
* if ( context.endsWith( 'bar foo' ) && childDefinition.name == 'listItem' ) {
* // Prevent next listeners from being called.
* evt.stop();
* // Set the checkChild()'s return value.
* evt.return = true;
* }
* }, { priority: 'high' } );
*
* @event checkChild
* @param {Array} args The `checkChild()`'s arguments.
*/
/**
* Event fired when the {@link #checkAttribute} method is called. It allows plugging in
* additional behavior, for example implementing rules which cannot be defined using the declarative
* {@link module:engine/model/schema~SchemaItemDefinition} interface.
*
* **Note:** The {@link #addAttributeCheck} method is a more handy way to register callbacks. Internally,
* it registers a listener to this event but comes with a simpler API and it is the recommended choice
* in most of the cases.
*
* 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 various ways, but the most important use case is overriding the standard behavior of the
* `checkAttribute()` method. Let's see a typical listener template:
*
* schema.on( 'checkAttribute', ( evt, args ) => {
* const context = args[ 0 ];
* const attributeName = args[ 1 ];
* }, { priority: 'high' } );
*
* The listener is added with a `high` priority to be executed before the default method is really called. The `args` callback
* 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 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:
*
* schema.on( 'checkAttribute', ( evt, args ) => {
* const context = args[ 0 ];
* const attributeName = args[ 1 ];
*
* if ( context.endsWith( 'heading1 $text' ) && attributeName == 'bold' ) {
* // Prevent next listeners from being called.
* evt.stop();
* // Set the checkAttribute()'s return value.
* evt.return = false;
* }
* }, { priority: 'high' } );
*
* 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:
*
* schema.on( 'checkAttribute', ( evt, args ) => {
* const context = args[ 0 ];
* const attributeName = args[ 1 ];
*
* if ( context.endsWith( 'bar foo $text' ) && attributeName == 'bold' ) {
* // Prevent next listeners from being called.
* evt.stop();
* // Set the checkAttribute()'s return value.
* evt.return = true;
* }
* }, { priority: 'high' } );
*
* @event checkAttribute
* @param {Array} args The `checkAttribute()`'s arguments.
*/
/**
* A definition of a {@link module:engine/model/schema~Schema schema} item.
*
* You can define the following rules:
*
* * {@link ~SchemaItemDefinition#allowIn `allowIn`} – Defines in which other items this item will be allowed.
* * {@link ~SchemaItemDefinition#allowAttributes `allowAttributes`} – Defines allowed attributes of the given item.
* * {@link ~SchemaItemDefinition#allowContentOf `allowContentOf`} – Inherits "allowed children" from other items.
* * {@link ~SchemaItemDefinition#allowWhere `allowWhere`} – Inherits "allowed in" from other items.
* * {@link ~SchemaItemDefinition#allowAttributesOf `allowAttributesOf`} – Inherits attributes from other items.
* * {@link ~SchemaItemDefinition#inheritTypesFrom `inheritTypesFrom`} – Inherits `is*` properties of other items.
* * {@link ~SchemaItemDefinition#inheritAllFrom `inheritAllFrom`} –
* A shorthand for `allowContentOf`, `allowWhere`, `allowAttributesOf`, `inheritTypesFrom`.
*
* # The `is*` properties
*
* There are a couple commonly used `is*` properties. Their role is to assign additional semantics to schema items.
* You can define more properties but you will also need to implement support for them in the existing editor features.
*
* * {@link ~SchemaItemDefinition#isBlock `isBlock`} – Whether this item is paragraph-like.
* Generally speaking, content is usually made out of blocks like paragraphs, list items, images, headings, etc.
* * {@link ~SchemaItemDefinition#isInline `isInline`} – Whether an item is "text-like" and should be treated as an inline node.
* Examples of inline elements: `$text`, `softBreak` (` `), etc.
* * {@link ~SchemaItemDefinition#isLimit `isLimit`} – It can be understood as whether this element
* should not be split by Enter . Examples of limit elements: `$root`, table cell, image caption, etc.
* In other words, all actions that happen inside a limit element are limited to its content.
* All objects are treated as limit elements, too.
* * {@link ~SchemaItemDefinition#isObject `isObject`} – Whether an item is "self-contained" and should be treated as a whole.
* Examples of object elements: `image`, `table`, `video`, etc. An object is also a limit, so
* {@link module:engine/model/schema~Schema#isLimit `isLimit()`} returns `true` for object elements automatically.
*
* 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.
*
* # Generic items
*
* There are three basic generic items: `$root`, `$block` and `$text`.
* They are defined as follows:
*
* this.schema.register( '$root', {
* isLimit: true
* } );
* this.schema.register( '$block', {
* allowIn: '$root',
* isBlock: true
* } );
* this.schema.register( '$text', {
* allowIn: '$block',
* isInline: true
* } );
*
* They reflect typical editor content that is contained within one root, consists of several blocks
* (paragraphs, lists items, headings, images) which, in turn, may contain text inside.
*
* By inheriting from the generic items you can define new items which will get extended by other editor features.
* Read more about generic types in the {@glink framework/guides/deep-dive/schema Schema deep dive} guide.
*
* # Example definitions
*
* Allow `paragraph` in roots and block quotes:
*
* schema.register( 'paragraph', {
* allowIn: [ '$root', 'blockQuote' ],
* isBlock: true
* } );
*
* Allow `paragraph` everywhere where `$block` is allowed (i.e. in `$root`):
*
* schema.register( 'paragraph', {
* allowWhere: '$block',
* isBlock: true
* } );
*
* Make `image` a block object, which is allowed everywhere where `$block` is.
* Also, allow `src` and `alt` attributes in it:
*
* schema.register( 'image', {
* allowWhere: '$block',
* allowAttributes: [ 'src', 'alt' ],
* isBlock: true,
* isObject: true
* } );
*
* Make `caption` allowed in `image` and make it allow all the content of `$block`s (usually, `$text`).
* Also, mark it as a limit element so it cannot be split:
*
* schema.register( 'caption', {
* allowIn: 'image',
* allowContentOf: '$block',
* isLimit: true
* } );
*
* Make `listItem` inherit all from `$block` but also allow additional attributes:
*
* schema.register( 'listItem', {
* inheritAllFrom: '$block',
* allowAttributes: [ 'listType', 'listIndent' ]
* } );
*
* Which translates to:
*
* schema.register( 'listItem', {
* allowWhere: '$block',
* allowContentOf: '$block',
* allowAttributesOf: '$block',
* inheritTypesFrom: '$block',
* allowAttributes: [ 'listType', 'listIndent' ]
* } );
*
* # Tips
*
* * Check schema definitions of existing features to see how they are defined.
* * If you want to publish your feature so other developers can use it, try to use
* generic items as much as possible.
* * Keep your model clean. Limit it to the actual data and store information in a normalized way.
* * Remember about defining the `is*` properties. They do not affect the allowed structures, but they can
* affect how the editor features treat your elements.
*
* @typedef {Object} module:engine/model/schema~SchemaItemDefinition
*
* @property {String|Array.} allowIn Defines in which other items this item will be allowed.
* @property {String|Array.} allowAttributes Defines allowed attributes of the given item.
* @property {String|Array.} allowContentOf Inherits "allowed children" from other items.
* @property {String|Array.} allowWhere Inherits "allowed in" from other items.
* @property {String|Array.} allowAttributesOf Inherits attributes from other items.
* @property {String|Array.} inheritTypesFrom Inherits `is*` properties of other items.
* @property {String} inheritAllFrom A shorthand for `allowContentOf`, `allowWhere`, `allowAttributesOf`, `inheritTypesFrom`.
*
* @property {Boolean} isBlock
* Whether this item is paragraph-like. Generally speaking, content is usually made out of blocks
* like paragraphs, list items, images, headings, etc. All these elements are marked as blocks. A block
* should not allow another block inside. Note: There is also the `$block` generic item which has `isBlock` set to `true`.
* Most block type items will inherit from `$block` (through `inheritAllFrom`).
*
* 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.
*
* @property {Boolean} isInline
* Whether an item is "text-like" and should be treated as an inline node. Examples of inline elements:
* `$text`, `softBreak` (` `), etc.
*
* 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.
*
* @property {Boolean} isLimit
* It can be understood as whether this element should not be split by Enter .
* Examples of limit elements: `$root`, table cell, image caption, etc. In other words, all actions that happen inside
* a limit element are limited to its content.
*
* 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.
*
* @property {Boolean} isObject
* Whether an item is "self-contained" and should be treated as a whole. Examples of object elements:
* `image`, `table`, `video`, etc.
*
* **Note:** An object is also a limit, so
* {@link module:engine/model/schema~Schema#isLimit `isLimit()`} returns `true` for object elements automatically.
*
* 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.
*
* @property {Boolean} isSelectable
* `true` when an element should be selectable as a whole by the user. Examples of selectable elements: `image`, `table`, `tableCell`, etc.
*
* **Note:** An object is also a selectable element, so
* {@link module:engine/model/schema~Schema#isSelectable `isSelectable()`} returns `true` for object elements automatically.
*
* Read more about selectable elements in the
* {@glink framework/guides/deep-dive/schema#selectable-elements Selectable elements} section of the Schema deep dive} guide.
*
* @property {Boolean} isContent
* An item is a content when it always finds its way to the editor data output regardless of the number and type of its descendants.
* Examples of content elements: `$text`, `image`, `table`, etc. (but not `paragraph`, `heading1` or `tableCell`).
*
* **Note:** An object is also a content element, so
* {@link module:engine/model/schema~Schema#isContent `isContent()`} returns `true` for object elements automatically.
*
* Read more about content elements in the
* {@glink framework/guides/deep-dive/schema#content-elements Content elements} section of the Schema deep dive} guide.
*/
/**
* A simplified version of {@link module:engine/model/schema~SchemaItemDefinition} after
* compilation by the {@link module:engine/model/schema~Schema schema}.
* Rules fed to the schema by {@link module:engine/model/schema~Schema#register}
* and {@link module:engine/model/schema~Schema#extend} methods are defined in the
* {@link module:engine/model/schema~SchemaItemDefinition} format.
* Later on, they are compiled to `SchemaCompiledItemDefinition` so when you use e.g.
* the {@link module:engine/model/schema~Schema#getDefinition} method you get the compiled version.
*
* The compiled version contains only the following properties:
*
* * The `name` property,
* * The `is*` properties,
* * The `allowIn` array,
* * The `allowAttributes` array.
*
* @typedef {Object} module:engine/model/schema~SchemaCompiledItemDefinition
*/
/**
* A schema context — a list of ancestors of a given position in the document.
*
* Considering such position:
*
* <$root>
*
*
* ^
*
*
* $root>
*
* The context of this position is its {@link module:engine/model/position~Position#getAncestors lists of ancestors}:
*
* [ rootElement, blockQuoteElement, paragraphElement ]
*
* Contexts are used in the {@link module:engine/model/schema~Schema#event:checkChild `Schema#checkChild`} and
* {@link module:engine/model/schema~Schema#event:checkAttribute `Schema#checkAttribute`} events as a definition
* of a place in the document where the check occurs. The context instances are created based on the first arguments
* of the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`} and
* {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} methods so when
* using these methods you need to use {@link module:engine/model/schema~SchemaContextDefinition}s.
*/
class SchemaContext {
/**
* Creates an instance of the context.
*
* @param {module:engine/model/schema~SchemaContextDefinition} context
*/
constructor( context ) {
if ( context instanceof SchemaContext ) {
return context;
}
if ( typeof context == 'string' ) {
context = [ context ];
} else if ( !Array.isArray( context ) ) {
// `context` is item or position.
// Position#getAncestors() doesn't accept any parameters but it works just fine here.
context = context.getAncestors( { includeSelf: true } );
}
if ( context[ 0 ] && typeof context[ 0 ] != 'string' && context[ 0 ].is( 'documentFragment' ) ) {
context.shift();
}
this._items = context.map( mapContextItem );
}
/**
* The number of items.
*
* @type {Number}
*/
get length() {
return this._items.length;
}
/**
* The last item (the lowest node).
*
* @type {module:engine/model/schema~SchemaContextItem}
*/
get last() {
return this._items[ this._items.length - 1 ];
}
/**
* Iterable interface.
*
* Iterates over all context items.
*
* @returns {Iterable.}
*/
[ Symbol.iterator ]() {
return this._items[ Symbol.iterator ]();
}
/**
* Returns a new schema context instance with an additional item.
*
* Item can be added as:
*
* const context = new SchemaContext( [ '$root' ] );
*
* // An element.
* const fooElement = writer.createElement( 'fooElement' );
* const newContext = context.push( fooElement ); // [ '$root', 'fooElement' ]
*
* // A text node.
* const text = writer.createText( 'foobar' );
* const newContext = context.push( text ); // [ '$root', '$text' ]
*
* // A string (element name).
* const newContext = context.push( 'barElement' ); // [ '$root', 'barElement' ]
*
* **Note** {@link module:engine/model/node~Node} that is already in the model tree will be added as the only item
* (without ancestors).
*
* @param {String|module:engine/model/node~Node|Array} item An item that will be added
* to the current context.
* @returns {module:engine/model/schema~SchemaContext} A new schema context instance with an additional item.
*/
push( item ) {
const ctx = new SchemaContext( [ item ] );
ctx._items = [ ...this._items, ...ctx._items ];
return ctx;
}
/**
* Gets an item on the given index.
*
* @returns {module:engine/model/schema~SchemaContextItem}
*/
getItem( index ) {
return this._items[ index ];
}
/**
* Returns the names of items.
*
* @returns {Iterable.}
*/
* getNames() {
yield* this._items.map( item => item.name );
}
/**
* Checks whether the context ends with the given nodes.
*
* const ctx = new SchemaContext( [ rootElement, paragraphElement, textNode ] );
*
* ctx.endsWith( '$text' ); // -> true
* ctx.endsWith( 'paragraph $text' ); // -> true
* ctx.endsWith( '$root' ); // -> false
* ctx.endsWith( 'paragraph' ); // -> false
*
* @param {String} query
* @returns {Boolean}
*/
endsWith( query ) {
return Array.from( this.getNames() ).join( ' ' ).endsWith( query );
}
/**
* Checks whether the context starts with the given nodes.
*
* const ctx = new SchemaContext( [ rootElement, paragraphElement, textNode ] );
*
* ctx.endsWith( '$root' ); // -> true
* ctx.endsWith( '$root paragraph' ); // -> true
* ctx.endsWith( '$text' ); // -> false
* ctx.endsWith( 'paragraph' ); // -> false
*
* @param {String} query
* @returns {Boolean}
*/
startsWith( query ) {
return Array.from( this.getNames() ).join( ' ' ).startsWith( query );
}
}
/**
* The definition of a {@link module:engine/model/schema~SchemaContext schema context}.
*
* Contexts can be created in multiple ways:
*
* * By defining a **node** – in this cases this node and all its ancestors will be used.
* * By defining a **position** in the document – in this case all its ancestors will be used.
* * By defining an **array of nodes** – in this case this array defines the entire context.
* * By defining a **name of node** - in this case node will be "mocked". It is not recommended because context
* will be unrealistic (e.g. attributes of these nodes are not specified). However, at times this may be the only
* way to define the context (e.g. when checking some hypothetical situation).
* * By defining an **array of node names** (potentially, mixed with real nodes) – The same as **name of node**
* but it is possible to create a path.
* * By defining a {@link module:engine/model/schema~SchemaContext} instance - in this case the same instance as provided
* will be return.
*
* Examples of context definitions passed to the {@link module:engine/model/schema~Schema#checkChild `Schema#checkChild()`}
* method:
*
* // Assuming that we have a $root > blockQuote > paragraph structure, the following code
* // will check node 'foo' in the following context:
* // [ rootElement, blockQuoteElement, paragraphElement ]
* const contextDefinition = paragraphElement;
* const childToCheck = 'foo';
* schema.checkChild( contextDefinition, childToCheck );
*
* // Also check in [ rootElement, blockQuoteElement, paragraphElement ].
* schema.checkChild( model.createPositionAt( paragraphElement, 0 ), 'foo' );
*
* // Check in [ rootElement, paragraphElement ].
* schema.checkChild( [ rootElement, paragraphElement ], 'foo' );
*
* // Check only fakeParagraphElement.
* schema.checkChild( 'paragraph', 'foo' );
*
* // Check in [ fakeRootElement, fakeBarElement, paragraphElement ].
* schema.checkChild( [ '$root', 'bar', paragraphElement ], 'foo' );
*
* All these `checkChild()` calls will fire {@link module:engine/model/schema~Schema#event:checkChild `Schema#checkChild`}
* events in which `args[ 0 ]` is an instance of the context. Therefore, you can write a listener like this:
*
* schema.on( 'checkChild', ( evt, args ) => {
* const ctx = args[ 0 ];
*
* console.log( Array.from( ctx.getNames() ) );
* } );
*
* Which will log the following:
*
* [ '$root', 'blockQuote', 'paragraph' ]
* [ '$root', 'paragraph' ]
* [ '$root', 'bar', 'paragraph' ]
*
* Note: When using the {@link module:engine/model/schema~Schema#checkAttribute `Schema#checkAttribute()`} method
* you may want to check whether a text node may have an attribute. A {@link module:engine/model/text~Text} is a
* correct way to define a context so you can do this:
*
* schema.checkAttribute( textNode, 'bold' );
*
* But sometimes you want to check whether a text at a given position might've had some attribute,
* in which case you can create a context by missing an array of elements with a `'$text'` string:
*
* // Check in [ rootElement, paragraphElement, textNode ].
* schema.checkChild( [ ...positionInParagraph.getAncestors(), '$text' ], 'bold' );
*
* @typedef {module:engine/model/node~Node|module:engine/model/position~Position|module:engine/model/schema~SchemaContext|
* String|Array.} module:engine/model/schema~SchemaContextDefinition
*/
/**
* An item of the {@link module:engine/model/schema~SchemaContext schema context}.
*
* It contains 3 properties:
*
* * `name` – the name of this item,
* * `* getAttributeKeys()` – a generator of keys of item attributes,
* * `getAttribute( keyName )` – a method to get attribute values.
*
* The context item interface is a highly simplified version of {@link module:engine/model/node~Node} and its role
* is to expose only the information which schema checks are able to provide (which is the name of the node and
* node's attributes).
*
* schema.on( 'checkChild', ( evt, args ) => {
* const ctx = args[ 0 ];
* const firstItem = ctx.getItem( 0 );
*
* console.log( firstItem.name ); // -> '$root'
* console.log( firstItem.getAttribute( 'foo' ) ); // -> 'bar'
* console.log( Array.from( firstItem.getAttributeKeys() ) ); // -> [ 'foo', 'faa' ]
* } );
*
* @typedef {Object} module:engine/model/schema~SchemaContextItem
*/
/**
* A structure containing additional metadata describing the attribute.
*
* See {@link module:engine/model/schema~Schema#setAttributeProperties `Schema#setAttributeProperties()`} for usage examples.
*
* @typedef {Object} module:engine/model/schema~AttributeProperties
* @property {Boolean} [isFormatting] Indicates that the attribute should be considered as a visual formatting, like `bold`, `italic` or
* `fontSize` rather than semantic attribute (such as `src`, `listType`, etc.). For example, it is used by the "Remove format" feature.
* @property {Boolean} [copyOnEnter] Indicates that given text attribute should be copied to the next block when enter is pressed.
*/
function compileBaseItemRule( sourceItemRules, itemName ) {
const itemRule = {
name: itemName,
allowIn: [],
allowContentOf: [],
allowWhere: [],
allowAttributes: [],
allowAttributesOf: [],
inheritTypesFrom: []
};
copyTypes( sourceItemRules, itemRule );
copyProperty( sourceItemRules, itemRule, 'allowIn' );
copyProperty( sourceItemRules, itemRule, 'allowContentOf' );
copyProperty( sourceItemRules, itemRule, 'allowWhere' );
copyProperty( sourceItemRules, itemRule, 'allowAttributes' );
copyProperty( sourceItemRules, itemRule, 'allowAttributesOf' );
copyProperty( sourceItemRules, itemRule, 'inheritTypesFrom' );
makeInheritAllWork( sourceItemRules, itemRule );
return itemRule;
}
function compileAllowContentOf( compiledDefinitions, itemName ) {
for ( const allowContentOfItemName of compiledDefinitions[ itemName ].allowContentOf ) {
// The allowContentOf property may point to an unregistered element.
if ( compiledDefinitions[ allowContentOfItemName ] ) {
const allowedChildren = getAllowedChildren( compiledDefinitions, allowContentOfItemName );
allowedChildren.forEach( allowedItem => {
allowedItem.allowIn.push( itemName );
} );
}
}
delete compiledDefinitions[ itemName ].allowContentOf;
}
function compileAllowWhere( compiledDefinitions, itemName ) {
for ( const allowWhereItemName of compiledDefinitions[ itemName ].allowWhere ) {
const inheritFrom = compiledDefinitions[ allowWhereItemName ];
// The allowWhere property may point to an unregistered element.
if ( inheritFrom ) {
const allowedIn = inheritFrom.allowIn;
compiledDefinitions[ itemName ].allowIn.push( ...allowedIn );
}
}
delete compiledDefinitions[ itemName ].allowWhere;
}
function compileAllowAttributesOf( compiledDefinitions, itemName ) {
for ( const allowAttributeOfItem of compiledDefinitions[ itemName ].allowAttributesOf ) {
const inheritFrom = compiledDefinitions[ allowAttributeOfItem ];
if ( inheritFrom ) {
const inheritAttributes = inheritFrom.allowAttributes;
compiledDefinitions[ itemName ].allowAttributes.push( ...inheritAttributes );
}
}
delete compiledDefinitions[ itemName ].allowAttributesOf;
}
function compileInheritPropertiesFrom( compiledDefinitions, itemName ) {
const item = compiledDefinitions[ itemName ];
for ( const inheritPropertiesOfItem of item.inheritTypesFrom ) {
const inheritFrom = compiledDefinitions[ inheritPropertiesOfItem ];
if ( inheritFrom ) {
const typeNames = Object.keys( inheritFrom ).filter( name => name.startsWith( 'is' ) );
for ( const name of typeNames ) {
if ( !( name in item ) ) {
item[ name ] = inheritFrom[ name ];
}
}
}
}
delete item.inheritTypesFrom;
}
// Remove items which weren't registered (because it may break some checks or we'd need to complicate them).
// Make sure allowIn doesn't contain repeated values.
function cleanUpAllowIn( compiledDefinitions, itemName ) {
const itemRule = compiledDefinitions[ itemName ];
const existingItems = itemRule.allowIn.filter( itemToCheck => compiledDefinitions[ itemToCheck ] );
itemRule.allowIn = Array.from( new Set( existingItems ) );
}
function cleanUpAllowAttributes( compiledDefinitions, itemName ) {
const itemRule = compiledDefinitions[ itemName ];
itemRule.allowAttributes = Array.from( new Set( itemRule.allowAttributes ) );
}
function copyTypes( sourceItemRules, itemRule ) {
for ( const sourceItemRule of sourceItemRules ) {
const typeNames = Object.keys( sourceItemRule ).filter( name => name.startsWith( 'is' ) );
for ( const name of typeNames ) {
itemRule[ name ] = sourceItemRule[ name ];
}
}
}
function copyProperty( sourceItemRules, itemRule, propertyName ) {
for ( const sourceItemRule of sourceItemRules ) {
if ( typeof sourceItemRule[ propertyName ] == 'string' ) {
itemRule[ propertyName ].push( sourceItemRule[ propertyName ] );
} else if ( Array.isArray( sourceItemRule[ propertyName ] ) ) {
itemRule[ propertyName ].push( ...sourceItemRule[ propertyName ] );
}
}
}
function makeInheritAllWork( sourceItemRules, itemRule ) {
for ( const sourceItemRule of sourceItemRules ) {
const inheritFrom = sourceItemRule.inheritAllFrom;
if ( inheritFrom ) {
itemRule.allowContentOf.push( inheritFrom );
itemRule.allowWhere.push( inheritFrom );
itemRule.allowAttributesOf.push( inheritFrom );
itemRule.inheritTypesFrom.push( inheritFrom );
}
}
}
function getAllowedChildren( compiledDefinitions, itemName ) {
const itemRule = compiledDefinitions[ itemName ];
return getValues( compiledDefinitions ).filter( def => def.allowIn.includes( itemRule.name ) );
}
function getValues( obj ) {
return Object.keys( obj ).map( key => obj[ key ] );
}
function mapContextItem( ctxItem ) {
if ( typeof ctxItem == 'string' ) {
return {
name: ctxItem,
* getAttributeKeys() {},
getAttribute() {}
};
} else {
return {
// '$text' means text nodes and text proxies.
name: ctxItem.is( 'element' ) ? ctxItem.name : '$text',
* getAttributeKeys() {
yield* ctxItem.getAttributeKeys();
},
getAttribute( key ) {
return ctxItem.getAttribute( key );
}
};
}
}
// Generator function returning values from provided walkers, switching between them at each iteration. If only one walker
// is provided it will return data only from that walker.
//
// @param {module:engine/module/treewalker~TreeWalker} [backward] Walker iterating in backward direction.
// @param {module:engine/module/treewalker~TreeWalker} [forward] Walker iterating in forward direction.
// @returns {Iterable.} Object returned at each iteration contains `value` and `walker` (informing which walker returned
// given value) fields.
function* combineWalkers( backward, forward ) {
let done = false;
while ( !done ) {
done = true;
if ( backward ) {
const step = backward.next();
if ( !step.done ) {
done = false;
yield {
walker: backward,
value: step.value
};
}
}
if ( forward ) {
const step = forward.next();
if ( !step.done ) {
done = false;
yield {
walker: forward,
value: step.value
};
}
}
}
}
// Takes an array of non-intersecting ranges. For each of them gets minimal flat ranges covering that range and returns
// all those minimal flat ranges.
//
// @param {Array.} ranges Ranges to process.
// @returns {Iterable.} Minimal flat ranges of given `ranges`.
function* convertToMinimalFlatRanges( ranges ) {
for ( const range of ranges ) {
yield* range.getMinimalFlatRanges();
}
}
function removeDisallowedAttributeFromNode( schema, node, writer ) {
for ( const attribute of node.getAttributeKeys() ) {
if ( !schema.checkAttribute( node, attribute ) ) {
writer.removeAttribute( attribute, node );
}
}
}
/***/ }),
/***/ "./node_modules/@ckeditor/ckeditor5-engine/src/model/selection.js":
/*!************************************************************************!*\
!*** ./node_modules/@ckeditor/ckeditor5-engine/src/model/selection.js ***!
\************************************************************************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return Selection; });
/* harmony import */ var _position__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./position */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/position.js");
/* harmony import */ var _node__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/node.js");
/* harmony import */ var _range__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./range */ "./node_modules/@ckeditor/ckeditor5-engine/src/model/range.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_emittermixin__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/emittermixin */ "./node_modules/@ckeditor/ckeditor5-utils/src/emittermixin.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/ckeditorerror */ "./node_modules/@ckeditor/ckeditor5-utils/src/ckeditorerror.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_mix__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/mix */ "./node_modules/@ckeditor/ckeditor5-utils/src/mix.js");
/* harmony import */ var _ckeditor_ckeditor5_utils_src_isiterable__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @ckeditor/ckeditor5-utils/src/isiterable */ "./node_modules/@ckeditor/ckeditor5-utils/src/isiterable.js");
/**
* @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 engine/model/selection
*/
/**
* Selection is a set of {@link module:engine/model/range~Range ranges}. It has a direction specified by its
* {@link module:engine/model/selection~Selection#anchor anchor} and {@link module:engine/model/selection~Selection#focus focus}
* (it can be {@link module:engine/model/selection~Selection#isBackward forward or backward}).
* Additionally, selection may have its own attributes (think – whether text typed in in this selection
* should have those attributes – e.g. whether you type a bolded text).
*
* @mixes module:utils/emittermixin~EmitterMixin
*/
class Selection {
/**
* Creates a new selection instance based on the given {@link module:engine/model/selection~Selectable selectable}
* or creates an empty selection if no arguments were passed.
*
* // Creates empty selection without ranges.
* const selection = writer.createSelection();
*
* // Creates selection at the given range.
* const range = writer.createRange( start, end );
* const selection = writer.createSelection( range );
*
* // Creates selection at the given ranges
* const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
* const selection = writer.createSelection( ranges );
*
* // Creates selection from the other selection.
* // Note: It doesn't copies selection attributes.
* const otherSelection = writer.createSelection();
* const selection = writer.createSelection( otherSelection );
*
* // Creates selection from the given document selection.
* // Note: It doesn't copies selection attributes.
* const documentSelection = model.document.selection;
* const selection = writer.createSelection( documentSelection );
*
* // Creates selection at the given position.
* const position = writer.createPositionFromPath( root, path );
* const selection = writer.createSelection( position );
*
* // Creates selection at the given offset in the given element.
* const paragraph = writer.createElement( 'paragraph' );
* const selection = writer.createSelection( paragraph, offset );
*
* // Creates a range inside an {@link module:engine/model/element~Element element} which starts before the
* // first child of that element and ends after the last child of that element.
* const selection = writer.createSelection( paragraph, 'in' );
*
* // Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends
* // just after the item.
* const selection = writer.createSelection( paragraph, 'on' );
*
* Selection's constructor allow passing additional options (`'backward'`) as the last argument.
*
* // Creates backward selection.
* const selection = writer.createSelection( range, { backward: true } );
*
* @param {module:engine/model/selection~Selectable} selectable
* @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
* @param {Object} [options]
* @param {Boolean} [options.backward] Sets this selection instance to be backward.
*/
constructor( selectable, placeOrOffset, options ) {
/**
* Specifies whether the last added range was added as a backward or forward range.
*
* @private
* @type {Boolean}
*/
this._lastRangeBackward = false;
/**
* Stores selection ranges.
*
* @protected
* @type {Array.}
*/
this._ranges = [];
/**
* List of attributes set on current selection.
*
* @protected
* @type {Map.}
*/
this._attrs = new Map();
if ( selectable ) {
this.setTo( selectable, placeOrOffset, options );
}
}
/**
* Selection anchor. Anchor is the position from which the selection was started. If a user is making a selection
* by dragging the mouse, the anchor is where the user pressed the mouse button (the beginning of the selection).
*
* Anchor and {@link #focus} define the direction of the selection, which is important
* when expanding/shrinking selection. The focus moves, while the anchor should remain in the same place.
*
* Anchor is always set to the {@link module:engine/model/range~Range#start start} or
* {@link module:engine/model/range~Range#end end} position of the last of selection's ranges. Whether it is
* the `start` or `end` depends on the specified `options.backward`. See the {@link #setTo `setTo()`} method.
*
* May be set to `null` if there are no ranges in the selection.
*
* @see #focus
* @readonly
* @type {module:engine/model/position~Position|null}
*/
get anchor() {
if ( this._ranges.length > 0 ) {
const range = this._ranges[ this._ranges.length - 1 ];
return this._lastRangeBackward ? range.end : range.start;
}
return null;
}
/**
* Selection focus. Focus is the position where the selection ends. If a user is making a selection
* by dragging the mouse, the focus is where the mouse cursor is.
*
* May be set to `null` if there are no ranges in the selection.
*
* @see #anchor
* @readonly
* @type {module:engine/model/position~Position|null}
*/
get focus() {
if ( this._ranges.length > 0 ) {
const range = this._ranges[ this._ranges.length - 1 ];
return this._lastRangeBackward ? range.start : range.end;
}
return null;
}
/**
* Whether the selection is collapsed. Selection is collapsed when there is exactly one range in it
* and it is collapsed.
*
* @readonly
* @type {Boolean}
*/
get isCollapsed() {
const length = this._ranges.length;
if ( length === 1 ) {
return this._ranges[ 0 ].isCollapsed;
} else {
return false;
}
}
/**
* Returns the number of ranges in the selection.
*
* @readonly
* @type {Number}
*/
get rangeCount() {
return this._ranges.length;
}
/**
* Specifies whether the selection's {@link #focus} precedes the selection's {@link #anchor}.
*
* @readonly
* @type {Boolean}
*/
get isBackward() {
return !this.isCollapsed && this._lastRangeBackward;
}
/**
* Checks whether this selection is equal to the given selection. Selections are equal if they have the same directions,
* the same number of ranges and all ranges from one selection equal to ranges from the another selection.
*
* @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} otherSelection
* Selection to compare with.
* @returns {Boolean} `true` if selections are equal, `false` otherwise.
*/
isEqual( otherSelection ) {
if ( this.rangeCount != otherSelection.rangeCount ) {
return false;
} else if ( this.rangeCount === 0 ) {
return true;
}
if ( !this.anchor.isEqual( otherSelection.anchor ) || !this.focus.isEqual( otherSelection.focus ) ) {
return false;
}
for ( const thisRange of this._ranges ) {
let found = false;
for ( const otherRange of otherSelection._ranges ) {
if ( thisRange.isEqual( otherRange ) ) {
found = true;
break;
}
}
if ( !found ) {
return false;
}
}
return true;
}
/**
* Returns an iterable object that iterates over copies of selection ranges.
*
* @returns {Iterable.}
*/
* getRanges() {
for ( const range of this._ranges ) {
yield new _range__WEBPACK_IMPORTED_MODULE_2__["default"]( range.start, range.end );
}
}
/**
* Returns a copy of the first range in the selection.
* First range is the one which {@link module:engine/model/range~Range#start start} position
* {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
* (not to confuse with the first range added to the selection).
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/range~Range|null}
*/
getFirstRange() {
let first = null;
for ( const range of this._ranges ) {
if ( !first || range.start.isBefore( first.start ) ) {
first = range;
}
}
return first ? new _range__WEBPACK_IMPORTED_MODULE_2__["default"]( first.start, first.end ) : null;
}
/**
* Returns a copy of the last range in the selection.
* Last range is the one which {@link module:engine/model/range~Range#end end} position
* {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
* recently added to the selection).
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/range~Range|null}
*/
getLastRange() {
let last = null;
for ( const range of this._ranges ) {
if ( !last || range.end.isAfter( last.end ) ) {
last = range;
}
}
return last ? new _range__WEBPACK_IMPORTED_MODULE_2__["default"]( last.start, last.end ) : null;
}
/**
* Returns the first position in the selection.
* First position is the position that {@link module:engine/model/position~Position#isBefore is before}
* any other position in the selection.
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/position~Position|null}
*/
getFirstPosition() {
const first = this.getFirstRange();
return first ? first.start.clone() : null;
}
/**
* Returns the last position in the selection.
* Last position is the position that {@link module:engine/model/position~Position#isAfter is after}
* any other position in the selection.
*
* Returns `null` if there are no ranges in selection.
*
* @returns {module:engine/model/position~Position|null}
*/
getLastPosition() {
const lastRange = this.getLastRange();
return lastRange ? lastRange.end.clone() : null;
}
/**
* Sets this selection's ranges and direction to the specified location based on the given
* {@link module:engine/model/selection~Selectable selectable}.
*
* // Removes all selection's ranges.
* selection.setTo( null );
*
* // Sets selection to the given range.
* const range = writer.createRange( start, end );
* selection.setTo( range );
*
* // Sets selection to given ranges.
* const ranges = [ writer.createRange( start1, end2 ), writer.createRange( star2, end2 ) ];
* selection.setTo( ranges );
*
* // Sets selection to other selection.
* // Note: It doesn't copies selection attributes.
* const otherSelection = writer.createSelection();
* selection.setTo( otherSelection );
*
* // Sets selection to the given document selection.
* // Note: It doesn't copies selection attributes.
* const documentSelection = new DocumentSelection( doc );
* selection.setTo( documentSelection );
*
* // Sets collapsed selection at the given position.
* const position = writer.createPositionFromPath( root, path );
* selection.setTo( position );
*
* // Sets collapsed selection at the position of the given node and an offset.
* selection.setTo( paragraph, offset );
*
* Creates a range inside an {@link module:engine/model/element~Element element} which starts before the first child of
* that element and ends after the last child of that element.
*
* selection.setTo( paragraph, 'in' );
*
* Creates a range on an {@link module:engine/model/item~Item item} which starts before the item and ends just after the item.
*
* selection.setTo( paragraph, 'on' );
*
* `Selection#setTo()`' method allow passing additional options (`backward`) as the last argument.
*
* // Sets backward selection.
* const selection = writer.createSelection( range, { backward: true } );
*
* @param {module:engine/model/selection~Selectable} selectable
* @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
* @param {Object} [options]
* @param {Boolean} [options.backward] Sets this selection instance to be backward.
*/
setTo( selectable, placeOrOffset, options ) {
if ( selectable === null ) {
this._setRanges( [] );
} else if ( selectable instanceof Selection ) {
this._setRanges( selectable.getRanges(), selectable.isBackward );
} else if ( selectable && typeof selectable.getRanges == 'function' ) {
// We assume that the selectable is a DocumentSelection.
// It can't be imported here, because it would lead to circular imports.
this._setRanges( selectable.getRanges(), selectable.isBackward );
} else if ( selectable instanceof _range__WEBPACK_IMPORTED_MODULE_2__["default"] ) {
this._setRanges( [ selectable ], !!placeOrOffset && !!placeOrOffset.backward );
} else if ( selectable instanceof _position__WEBPACK_IMPORTED_MODULE_0__["default"] ) {
this._setRanges( [ new _range__WEBPACK_IMPORTED_MODULE_2__["default"]( selectable ) ] );
} else if ( selectable instanceof _node__WEBPACK_IMPORTED_MODULE_1__["default"] ) {
const backward = !!options && !!options.backward;
let range;
if ( placeOrOffset == 'in' ) {
range = _range__WEBPACK_IMPORTED_MODULE_2__["default"]._createIn( selectable );
} else if ( placeOrOffset == 'on' ) {
range = _range__WEBPACK_IMPORTED_MODULE_2__["default"]._createOn( selectable );
} else if ( placeOrOffset !== undefined ) {
range = new _range__WEBPACK_IMPORTED_MODULE_2__["default"]( _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( selectable, placeOrOffset ) );
} else {
/**
* selection.setTo requires the second parameter when the first parameter is a node.
*
* @error model-selection-setto-required-second-parameter
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_4__["default"]( 'model-selection-setto-required-second-parameter', [ this, selectable ] );
}
this._setRanges( [ range ], backward );
} else if ( Object(_ckeditor_ckeditor5_utils_src_isiterable__WEBPACK_IMPORTED_MODULE_6__["default"])( selectable ) ) {
// We assume that the selectable is an iterable of ranges.
this._setRanges( selectable, placeOrOffset && !!placeOrOffset.backward );
} else {
/**
* Cannot set the selection to the given place.
*
* Invalid parameters were specified when setting the selection. Common issues:
*
* * A {@link module:engine/model/textproxy~TextProxy} instance was passed instead of
* a real {@link module:engine/model/text~Text}.
* * View nodes were passed instead of model nodes.
* * `null`/`undefined` was passed.
*
* @error model-selection-setto-not-selectable
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_4__["default"]( 'model-selection-setto-not-selectable', [ this, selectable ] );
}
}
/**
* Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
* is treated like the last added range and is used to set {@link module:engine/model/selection~Selection#anchor} and
* {@link module:engine/model/selection~Selection#focus}. Accepts a flag describing in which direction the selection is made.
*
* @protected
* @fires change:range
* @param {Iterable.} newRanges Ranges to set.
* @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end (`false`)
* or backward - from end to start (`true`).
*/
_setRanges( newRanges, isLastBackward = false ) {
newRanges = Array.from( newRanges );
// Check whether there is any range in new ranges set that is different than all already added ranges.
const anyNewRange = newRanges.some( newRange => {
if ( !( newRange instanceof _range__WEBPACK_IMPORTED_MODULE_2__["default"] ) ) {
/**
* Selection range set to an object that is not an instance of {@link module:engine/model/range~Range}.
*
* Only {@link module:engine/model/range~Range} instances can be used to set a selection.
* Common mistakes leading to this error are:
*
* * using DOM `Range` object,
* * incorrect CKEditor 5 installation with multiple `ckeditor5-engine` packages having different versions.
*
* @error model-selection-set-ranges-not-range
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_4__["default"](
'model-selection-set-ranges-not-range',
[ this, newRanges ]
);
}
return this._ranges.every( oldRange => {
return !oldRange.isEqual( newRange );
} );
} );
// Don't do anything if nothing changed.
if ( newRanges.length === this._ranges.length && !anyNewRange ) {
return;
}
this._removeAllRanges();
for ( const range of newRanges ) {
this._pushRange( range );
}
this._lastRangeBackward = !!isLastBackward;
this.fire( 'change:range', { directChange: true } );
}
/**
* Moves {@link module:engine/model/selection~Selection#focus} to the specified location.
*
* The location can be specified in the same form as
* {@link module:engine/model/writer~Writer#createPositionAt writer.createPositionAt()} parameters.
*
* @fires change:range
* @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
* @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
* first parameter is a {@link module:engine/model/item~Item model item}.
*/
setFocus( itemOrPosition, offset ) {
if ( this.anchor === null ) {
/**
* Cannot set selection focus if there are no ranges in selection.
*
* @error model-selection-setfocus-no-ranges
*/
throw new _ckeditor_ckeditor5_utils_src_ckeditorerror__WEBPACK_IMPORTED_MODULE_4__["default"]( 'model-selection-setfocus-no-ranges', [ this, itemOrPosition ] );
}
const newFocus = _position__WEBPACK_IMPORTED_MODULE_0__["default"]._createAt( itemOrPosition, offset );
if ( newFocus.compareWith( this.focus ) == 'same' ) {
return;
}
const anchor = this.anchor;
if ( this._ranges.length ) {
this._popRange();
}
if ( newFocus.compareWith( anchor ) == 'before' ) {
this._pushRange( new _range__WEBPACK_IMPORTED_MODULE_2__["default"]( newFocus, anchor ) );
this._lastRangeBackward = true;
} else {
this._pushRange( new _range__WEBPACK_IMPORTED_MODULE_2__["default"]( anchor, newFocus ) );
this._lastRangeBackward = false;
}
this.fire( 'change:range', { directChange: true } );
}
/**
* Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
*
* @param {String} key Key of attribute to look for.
* @returns {*} Attribute value or `undefined`.
*/
getAttribute( key ) {
return this._attrs.get( key );
}
/**
* Returns iterable that iterates over this selection's attributes.
*
* Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
* This format is accepted by native `Map` object and also can be passed in `Node` constructor.
*
* @returns {Iterable.<*>}
*/
getAttributes() {
return this._attrs.entries();
}
/**
* Returns iterable that iterates over this selection's attribute keys.
*
* @returns {Iterable.}
*/
getAttributeKeys() {
return this._attrs.keys();
}
/**
* Checks if the selection has an attribute for given key.
*
* @param {String} key Key of attribute to check.
* @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
*/
hasAttribute( key ) {
return this._attrs.has( key );
}
/**
* Removes an attribute with given key from the selection.
*
* If given attribute was set on the selection, fires the {@link #event:change:range} event with
* removed attribute key.
*
* @fires change:attribute
* @param {String} key Key of attribute to remove.
*/
removeAttribute( key ) {
if ( this.hasAttribute( key ) ) {
this._attrs.delete( key );
this.fire( 'change:attribute', { attributeKeys: [ key ], directChange: true } );
}
}
/**
* Sets attribute on the selection. If attribute with the same key already is set, it's value is overwritten.
*
* If the attribute value has changed, fires the {@link #event:change:range} event with
* the attribute key.
*
* @fires change:attribute
* @param {String} key Key of attribute to set.
* @param {*} value Attribute value.
*/
setAttribute( key, value ) {
if ( this.getAttribute( key ) !== value ) {
this._attrs.set( key, value );
this.fire( 'change:attribute', { attributeKeys: [ key ], directChange: true } );
}
}
/**
* Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
* one range in the selection, and that range contains exactly one element.
* Returns `null` if there is no selected element.
*
* @returns {module:engine/model/element~Element|null}
*/
getSelectedElement() {
if ( this.rangeCount !== 1 ) {
return null;
}
return this.getFirstRange().getContainedElement();
}
/**
* Checks whether this object is of the given.
*
* selection.is( 'selection' ); // -> true
* selection.is( 'model:selection' ); // -> true
*
* selection.is( 'view:selection' ); // -> false
* selection.is( 'range' ); // -> false
*
* {@link module:engine/model/node~Node#is Check the entire list of model objects} which implement the `is()` method.
*
* @param {String} type
* @returns {Boolean}
*/
is( type ) {
return type === 'selection' || type === 'model:selection';
}
/**
* Gets elements of type {@link module:engine/model/schema~Schema#isBlock "block"} touched by the selection.
*
* This method's result can be used for example to apply block styling to all blocks covered by this selection.
*
* **Note:** `getSelectedBlocks()` returns blocks that are nested in other non-block elements
* but will not return blocks nested in other blocks.
*
* In this case the function will return exactly all 3 paragraphs (note: `