Launch Week Day 5: Introducing Reachability for PHP.Learn More
Socket
Book a DemoSign in
Socket

@ckeditor/ckeditor5-engine

Package Overview
Dependencies
Maintainers
1
Versions
1587
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ckeditor/ckeditor5-engine - npm Package Compare versions

Comparing version
20.0.0
to
21.0.0
+145
src/view/rawelement.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/view/rawelement
*/
import Element from './element';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import Node from './node';
/**
* The raw element class.
*
* Raw elements work as data containers ("wrappers", "sandboxes") but their children are not managed or
* even recognized by the editor. This encapsulation allows integrations to maintain custom DOM structures
* in the editor content without, for instance, worrying about compatibility with other editor features.
* Raw elements make a perfect tool for integration with external frameworks and data sources.
*
* Unlike {@link module:engine/view/uielement~UIElement ui elements}, raw elements act like a real editor
* content (similar to {@link module:engine/view/containerelement~ContainerElement} or
* {@link module:engine/view/emptyelement~EmptyElement}), they are considered by the editor selection and
* {@link module:widget/utils~toWidget they can work as widgets}.
*
* To create a new raw element use the
* {@link module:engine/view/downcastwriter~DowncastWriter#createRawElement `downcastWriter#createRawElement()`} method.
*
* @extends module:engine/view/element~Element
*/
export default class RawElement extends Element {
/**
* Creates new instance of RawElement.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-rawelement-cannot-add` when `children` parameter
* is passed, to inform that usage of `RawElement` is incorrect (adding child nodes to `RawElement` is forbidden).
*
* @see module:engine/view/downcastwriter~DowncastWriter#createRawElement
* @protected
* @param {module:engine/view/document~Document} document The document instance to which this element belongs.
* @param {String} name Node name.
* @param {Object|Iterable} [attrs] Collection of attributes.
* @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
* A list of nodes to be inserted into created element.
*/
constructor( document, name, attrs, children ) {
super( document, name, attrs, children );
/**
* Returns `null` because filler is not needed for RawElements.
*
* @method #getFillerOffset
* @returns {null} Always returns null.
*/
this.getFillerOffset = getFillerOffset;
}
/**
* Checks whether this object is of the given type or name.
*
* rawElement.is( 'rawElement' ); // -> true
* rawElement.is( 'element' ); // -> true
* rawElement.is( 'node' ); // -> true
* rawElement.is( 'view:rawElement' ); // -> true
* rawElement.is( 'view:element' ); // -> true
* rawElement.is( 'view:node' ); // -> true
*
* rawElement.is( 'model:element' ); // -> false
* rawElement.is( 'documentFragment' ); // -> false
*
* Assuming that the object being checked is a raw element, you can also check its
* {@link module:engine/view/rawelement~RawElement#name name}:
*
* rawElement.is( 'img' ); // -> true if this is a img element
* rawElement.is( 'rawElement', 'img' ); // -> same as above
* text.is( 'img' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} [name] Element name.
* @returns {Boolean}
*/
is( type, name = null ) {
if ( !name ) {
return type === 'rawElement' || type === 'view:rawElement' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||
type === 'node' || type === 'view:node';
} else {
return name === this.name && (
type === 'rawElement' || type === 'view:rawElement' ||
type === 'element' || type === 'view:element'
);
}
}
/**
* Overrides {@link module:engine/view/element~Element#_insertChild} method.
* Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-rawelement-cannot-add` to prevent
* adding any child nodes to a `RawElement`.
*
* @protected
*/
_insertChild( index, nodes ) {
if ( nodes && ( nodes instanceof Node || Array.from( nodes ).length > 0 ) ) {
/**
* Cannot add children to a {@link module:engine/view/rawelement~RawElement} instance.
*
* @error view-rawelement-cannot-add
*/
throw new CKEditorError(
'view-rawelement-cannot-add: Cannot add child nodes to a RawElement instance.',
[ this, nodes ]
);
}
}
/**
* Allows rendering the children of a {@link module:engine/view/rawelement~RawElement} on the DOM level.
* This method is called by the {@link module:engine/view/domconverter~DomConverter} with the raw DOM element
* passed as an argument leaving the number and shape of the children up to the integrator.
*
* This method **must be defined** for the `RawElement` to work:
*
* const myRawElement = downcastWriter.createRawElement( 'div' );
*
* myRawElement.render = function( domElement ) {
* domElement.innerHTML = '<b>This is the raw content of myRawElement.</b>';
* };
*
* @method #render
* @param {HTMLElement} domElement The native DOM element representing the raw view element.
*/
}
// Returns `null` because block filler is not needed for RawElements.
//
// @returns {null}
function getFillerOffset() {
return null;
}
+17
-17
{
"name": "@ckeditor/ckeditor5-engine",
"version": "20.0.0",
"version": "21.0.0",
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.",

@@ -24,21 +24,21 @@ "keywords": [

"dependencies": {
"@ckeditor/ckeditor5-utils": "^20.0.0",
"@ckeditor/ckeditor5-utils": "^21.0.0",
"lodash-es": "^4.17.15"
},
"devDependencies": {
"@ckeditor/ckeditor5-basic-styles": "^20.0.0",
"@ckeditor/ckeditor5-block-quote": "^20.0.0",
"@ckeditor/ckeditor5-core": "^20.0.0",
"@ckeditor/ckeditor5-editor-classic": "^20.0.0",
"@ckeditor/ckeditor5-enter": "^20.0.0",
"@ckeditor/ckeditor5-essentials": "^20.0.0",
"@ckeditor/ckeditor5-heading": "^20.0.0",
"@ckeditor/ckeditor5-link": "^20.0.0",
"@ckeditor/ckeditor5-list": "^20.0.0",
"@ckeditor/ckeditor5-paragraph": "^20.0.0",
"@ckeditor/ckeditor5-table": "^20.0.0",
"@ckeditor/ckeditor5-theme-lark": "^20.0.0",
"@ckeditor/ckeditor5-typing": "^20.0.0",
"@ckeditor/ckeditor5-undo": "^20.0.0",
"@ckeditor/ckeditor5-widget": "^20.0.0"
"@ckeditor/ckeditor5-basic-styles": "^21.0.0",
"@ckeditor/ckeditor5-block-quote": "^21.0.0",
"@ckeditor/ckeditor5-core": "^21.0.0",
"@ckeditor/ckeditor5-editor-classic": "^21.0.0",
"@ckeditor/ckeditor5-enter": "^21.0.0",
"@ckeditor/ckeditor5-essentials": "^21.0.0",
"@ckeditor/ckeditor5-heading": "^21.0.0",
"@ckeditor/ckeditor5-link": "^21.0.0",
"@ckeditor/ckeditor5-list": "^21.0.0",
"@ckeditor/ckeditor5-paragraph": "^21.0.0",
"@ckeditor/ckeditor5-table": "^21.0.0",
"@ckeditor/ckeditor5-theme-lark": "^21.0.0",
"@ckeditor/ckeditor5-typing": "^21.0.0",
"@ckeditor/ckeditor5-undo": "^21.0.0",
"@ckeditor/ckeditor5-widget": "^21.0.0"
},

@@ -45,0 +45,0 @@ "engines": {

@@ -93,3 +93,4 @@ /**

this.downcastDispatcher = new DowncastDispatcher( {
mapper: this.mapper
mapper: this.mapper,
schema: model.schema
} );

@@ -136,2 +137,3 @@ this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );

this.decorate( 'init' );
this.decorate( 'set' );

@@ -321,2 +323,3 @@ // Fire `ready` event when initialisation has completed. Such low level listener gives possibility

*
* @fires set
* @param {String|Object.<String,String>} data Input data as a string or an object containing `rootName` - `data`

@@ -457,2 +460,11 @@ * pairs to set data on multiple roots at once.

*/
/**
* Event fired after {@link #set set() method} has been run.
*
* The `set` event is fired by decorated {@link #set} method.
* See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples.
*
* @event set
*/
}

@@ -459,0 +471,0 @@

@@ -68,3 +68,4 @@ /**

this.downcastDispatcher = new DowncastDispatcher( {
mapper: this.mapper
mapper: this.mapper,
schema: model.schema
} );

@@ -71,0 +72,0 @@

@@ -340,3 +340,3 @@ /**

*
* if ( viewElement.is( 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
* if ( viewElement.is( 'element', 'span' ) && fontWeight && /\d+/.test() && Number( fontWeight ) > 500 ) {
* // Returned value can be an object with the matched properties.

@@ -392,3 +392,3 @@ * // These properties will be "consumed" during the conversion.

*
* if ( viewElement.is( 'span' ) && size > 10 ) {
* if ( viewElement.is( 'element', 'span' ) && size > 10 ) {
* // Returned value can be an object with the matched properties.

@@ -418,3 +418,3 @@ * // These properties will be "consumed" during the conversion.

*
* if ( viewElement.is( 'span' ) && size < 10 ) {
* if ( viewElement.is( 'element', 'span' ) && size < 10 ) {
* // Returned value can be an object with the matched properties.

@@ -421,0 +421,0 @@ * // These properties will be "consumed" during the conversion.

@@ -662,2 +662,8 @@ /**

/**
* The {@link module:engine/model/schema~Schema} instance set for the model that is downcast.
*
* @member {module:engine/model/schema~Schema} #schema
*/
/**
* The {@link module:engine/view/downcastwriter~DowncastWriter} instance used to manipulate data during conversion.

@@ -664,0 +670,0 @@ *

@@ -107,3 +107,3 @@ /**

data.viewPosition = this._findPositionIn( viewContainer, data.modelPosition.offset );
data.viewPosition = this.findPositionIn( viewContainer, data.modelPosition.offset );
}, { priority: 'low' } );

@@ -449,3 +449,3 @@

// If the position is a text it is simple ("ba|r" -> 2).
if ( viewParent.is( 'text' ) ) {
if ( viewParent.is( '$text' ) ) {
return viewOffset;

@@ -493,3 +493,3 @@ }

return 1;
} else if ( viewNode.is( 'text' ) ) {
} else if ( viewNode.is( '$text' ) ) {
return viewNode.data.length;

@@ -516,3 +516,3 @@ } else if ( viewNode.is( 'uiElement' ) ) {

*
* _findPositionIn( p, 4 ):
* findPositionIn( p, 4 ):
* <p>|fo<b>bar</b>bom</p> -> expected offset: 4, actual offset: 0

@@ -522,11 +522,10 @@ * <p>fo|<b>bar</b>bom</p> -> expected offset: 4, actual offset: 2

*
* _findPositionIn( b, 4 - ( 5 - 3 ) ):
* findPositionIn( b, 4 - ( 5 - 3 ) ):
* <p>fo<b>|bar</b>bom</p> -> expected offset: 2, actual offset: 0
* <p>fo<b>bar|</b>bom</p> -> expected offset: 2, actual offset: 3 -> we are too far
*
* _findPositionIn( bar, 2 - ( 3 - 3 ) ):
* findPositionIn( bar, 2 - ( 3 - 3 ) ):
* We are in the text node so we can simple find the offset.
* <p>fo<b>ba|r</b>bom</p> -> expected offset: 2, actual offset: 2 -> position found
*
* @private
* @param {module:engine/view/element~Element} viewParent Tree view element in which we are looking for the position.

@@ -536,3 +535,3 @@ * @param {Number} expectedOffset Expected offset.

*/
_findPositionIn( viewParent, expectedOffset ) {
findPositionIn( viewParent, expectedOffset ) {
// Last scanned view node.

@@ -547,3 +546,3 @@ let viewNode;

// In the text node it is simple: offset in the model equals offset in the text.
if ( viewParent.is( 'text' ) ) {
if ( viewParent.is( '$text' ) ) {
return new ViewPosition( viewParent, expectedOffset );

@@ -570,3 +569,3 @@ }

// so we subtract it from the expected offset to fine the offset in the child.
return this._findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) );
return this.findPositionIn( viewNode, expectedOffset - ( modelOffset - lastLength ) );
}

@@ -643,3 +642,3 @@ }

* // Check if this is the element we are interested in.
* if ( !sibling.is( 'customElement' ) ) {
* if ( !sibling.is( 'element', 'customElement' ) ) {
* return;

@@ -646,0 +645,0 @@ * }

@@ -240,3 +240,3 @@ /**

this.fire( 'element:' + viewItem.name, data, this.conversionApi );
} else if ( viewItem.is( 'text' ) ) {
} else if ( viewItem.is( '$text' ) ) {
this.fire( 'text', data, this.conversionApi );

@@ -243,0 +243,0 @@ } else {

@@ -12,3 +12,8 @@ /**

import ModelSelection from '../model/selection';
import { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
/* global console */
/**

@@ -291,2 +296,4 @@ * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for

*
* **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:

@@ -326,2 +333,3 @@ * `<p>Fo<span data-marker="comment" data-comment-id="7"></span>o</p><p>B<span data-marker="comment" data-comment-id="7"></span>ar</p>`,

*
* @deprecated
* @method #elementToMarker

@@ -336,4 +344,89 @@ * @param {Object} config Conversion configuration.

elementToMarker( config ) {
/**
* The {@link module:engine/conversion/upcasthelpers~UpcastHelpers#elementToMarker `UpcastHelpers#elementToMarker()`}
* method has been 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
*/
console.warn(
attachLinkToDocumentation(
'upcast-helpers-element-to-marker-deprecated: ' +
'The UpcastHelpers#elementToMarker() method has been deprecated and will be removed in the near future. ' +
'Please use UpcastHelpers#dataToMarker() instead.'
)
);
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 `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 `comment` group will be converted.
* // The conversion will look for `<comment-start>` and `<comment-end>` 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:
* <p>Foo<comment-start name="commentId:uid"></comment-start>bar</p>
* <figure data-comment-end-after="commentId:uid" class="image"><img src="abc.jpg" /></figure>
*
* // Model:
* <paragraph>Foo[bar</paragraph>
* <image src="abc.jpg"></image>]
*
* Where `[]` are boundaries of a marker that will receive `comment:commentId:uid` name.
*
* Other examples of usage:
*
* // Using custom function which is the same as the default conversion:
* editor.conversion.for( 'upcast' ).dataToMarker( {
* view: 'comment',
* model: name => 'comment:' + name,
* } );
*
* // Using converter priority:
* editor.conversion.for( 'upcast' ).dataToMarker( {
* view: 'comment',
* model: name => '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 Marker group name to convert.
* @param {Function} [config.model] Function that takes `name` part from the view element or attribute 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 ) );
}
}

@@ -519,3 +612,3 @@

normalizeToMarkerConfig( config );
normalizeElementToMarkerConfig( config );

@@ -525,2 +618,105 @@ 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 = 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 = priorities.get( 'low' );
const maxPriority = priorities.get( 'highest' );
const priorityFactor = priorities.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 );
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

@@ -788,6 +984,6 @@ // and if so, returns it.

// 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.
// function and converts it to a format that is supported by `upcastElementToElement()` function.
//
// @param {Object} config Conversion configuration.
function normalizeToMarkerConfig( config ) {
function normalizeElementToMarkerConfig( config ) {
const oldModel = config.model;

@@ -801,1 +997,21 @@

}
// 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 <markerGroup-start> and <markerGroup-end> elements.
configForElements.view = config.view + '-' + type;
configForElements.model = ( viewElement, modelWriter ) => {
const viewName = viewElement.getAttribute( 'name' );
const markerName = config.model( viewName );
return modelWriter.createElement( '$marker', { 'data-name': markerName } );
};
return configForElements;
}

@@ -83,3 +83,3 @@ /**

// For text nodes and document fragments just mark them as consumable.
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
this._consumables.set( element, true );

@@ -138,3 +138,3 @@

// For text nodes and document fragments return stored boolean value.
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
return elementConsumables;

@@ -177,3 +177,3 @@ }

if ( this.test( element, consumables ) ) {
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
// For text nodes and document fragments set value to false.

@@ -224,3 +224,3 @@ this._consumables.set( element, false );

if ( elementConsumables !== undefined ) {
if ( element.is( 'text' ) || element.is( 'documentFragment' ) ) {
if ( element.is( '$text' ) || element.is( 'documentFragment' ) ) {
// For text nodes and document fragments - set consumable to true.

@@ -294,3 +294,3 @@ this._consumables.set( element, true );

if ( from.is( 'text' ) ) {
if ( from.is( '$text' ) ) {
instance.add( from );

@@ -297,0 +297,0 @@

@@ -231,3 +231,3 @@ /**

downcastDispatcher.on( 'attribute', ( evt, data, conversionApi ) => {
if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( 'textProxy' ) ) {
if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection || data.item.is( '$textProxy' ) ) {
const converter = wrap( ( modelAttributeValue, viewWriter ) => {

@@ -234,0 +234,0 @@ return viewWriter.createAttributeElement(

@@ -28,2 +28,3 @@ /**

import UIElement from '../view/uielement';
import RawElement from '../view/rawelement';
import { StylesProcessor } from '../view/stylesmap';

@@ -39,3 +40,4 @@

'empty': EmptyElement,
'ui': UIElement
'ui': UIElement,
'raw': RawElement
};

@@ -60,2 +62,4 @@

* {@link module:engine/view/uielement~UIElement} will be printed.
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each
* {@link module:engine/view/rawelement~RawElement} will be printed.
* @returns {String} The stringified data.

@@ -76,2 +80,3 @@ */

renderUIElements: options.renderUIElements,
renderRawElements: options.renderRawElements,
ignoreRoot: true

@@ -241,2 +246,4 @@ };

* {@link module:engine/view/uielement~UIElement} will be printed.
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each
* {@link module:engine/view/rawelement~RawElement} will be printed.
* @returns {String} An HTML-like string representing the view.

@@ -460,3 +467,3 @@ */

if ( node.is( 'text' ) ) {
if ( node.is( '$text' ) ) {
const regexp = new RegExp(

@@ -631,2 +638,4 @@ `[${ TEXT_RANGE_START_TOKEN }${ TEXT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_END_TOKEN }\\${ ELEMENT_RANGE_START_TOKEN }]`,

* {@link module:engine/view/uielement~UIElement} will be printed.
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each
* {@link module:engine/view/rawelement~RawElement} will be printed.
*/

@@ -648,2 +657,3 @@ constructor( root, selection, options ) {

this.renderUIElements = !!options.renderUIElements;
this.renderRawElements = !!options.renderRawElements;
}

@@ -681,4 +691,11 @@

if ( this.renderUIElements && root.is( 'uiElement' ) ) {
if ( ( this.renderUIElements && root.is( 'uiElement' ) ) ) {
callback( root.render( document ).innerHTML );
} else if ( this.renderRawElements && root.is( 'rawElement' ) ) {
// There's no DOM element for "root" to pass to render(). Creating
// a surrogate container to render the children instead.
const rawContentContainer = document.createElement( 'div' );
root.render( rawContentContainer );
callback( rawContentContainer.innerHTML );
} else {

@@ -700,3 +717,3 @@ let offset = 0;

if ( root.is( 'text' ) ) {
if ( root.is( '$text' ) ) {
callback( this._stringifyTextRanges( root ) );

@@ -837,4 +854,5 @@ }

* * 'container' for {@link module:engine/view/containerelement~ContainerElement container elements},
* * 'empty' for {@link module:engine/view/emptyelement~EmptyElement empty elements}.
* * 'ui' for {@link module:engine/view/uielement~UIElement UI elements}.
* * 'empty' for {@link module:engine/view/emptyelement~EmptyElement empty elements},
* * 'ui' for {@link module:engine/view/uielement~UIElement UI elements},
* * 'raw' for {@link module:engine/view/rawelement~RawElement raw elements},
* * an empty string when the current configuration is preventing showing elements' types.

@@ -957,6 +975,6 @@ *

throw new Error( 'Parse error - cannot parse inside EmptyElement.' );
}
if ( convertedElement.is( 'uiElement' ) ) {
} else if ( convertedElement.is( 'uiElement' ) ) {
throw new Error( 'Parse error - cannot parse inside UIElement.' );
} else if ( convertedElement.is( 'rawElement' ) ) {
throw new Error( 'Parse error - cannot parse inside RawElement.' );
}

@@ -963,0 +981,0 @@

@@ -1042,3 +1042,3 @@ /**

for ( const child of children ) {
if ( child.is( 'text' ) ) {
if ( child.is( '$text' ) ) {
for ( let i = 0; i < child.data.length; i++ ) {

@@ -1045,0 +1045,0 @@ snapshot.push( {

@@ -338,3 +338,3 @@ /**

// @if CK_DEBUG_ENGINE // if ( child.is( 'text' ) ) {
// @if CK_DEBUG_ENGINE // if ( child.is( '$text' ) ) {
// @if CK_DEBUG_ENGINE // const textAttrs = stringifyMap( child._attrs );

@@ -341,0 +341,0 @@

@@ -570,3 +570,2 @@ /**

//
class LiveSelection extends Selection {

@@ -606,6 +605,6 @@ // Creates an empty live selection for given {@link module:engine/model/document~Document}.

// Contains data required to fix ranges which have been moved to the graveyard.
// Position to which the selection should be set if the last selection range was moved to the graveyard.
// @private
// @member {Array} module:engine/model/liveselection~LiveSelection#_fixGraveyardRangesData
this._fixGraveyardRangesData = [];
// @member {module:engine/model/position~Position} module:engine/model/liveselection~LiveSelection#_selectionRestorePosition
this._selectionRestorePosition = null;

@@ -633,8 +632,10 @@ // Flag that informs whether the selection ranges have changed. It is changed on true when `LiveRange#change:range` event is fired.

while ( this._fixGraveyardRangesData.length ) {
const { liveRange, sourcePosition } = this._fixGraveyardRangesData.shift();
this._fixGraveyardSelection( liveRange, sourcePosition );
// 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 ) {

@@ -833,11 +834,13 @@ this._hasChangedRange = false;

// 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` is in whole moved to the graveyard, save necessary data. It will be fixed on `Model#applyOperation` event.
if ( liveRange.root == this._document.graveyard ) {
this._fixGraveyardRangesData.push( {
liveRange,
sourcePosition: data.deletionPosition
} );
this._selectionRestorePosition = data.deletionPosition;
const index = this._ranges.indexOf( liveRange );
this._ranges.splice( index, 1 );
liveRange.detach();
}

@@ -1124,32 +1127,16 @@ } );

// Fixes a selection range after it ends up in graveyard root.
// Fixes the selection after all its ranges got removed.
//
// @private
// @param {module:engine/model/liverange~LiveRange} liveRange The range from selection, that ended up in the graveyard root.
// @param {module:engine/model/position~Position} removedRangeStart Start position of a range which was removed.
_fixGraveyardSelection( liveRange, removedRangeStart ) {
// The start of the removed range is the closest position to the `liveRange` - the original selection range.
// This is a good candidate for a fixed selection range.
const positionCandidate = removedRangeStart.clone();
// @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 );
// Find a range that is a correct selection range and is closest to the start of removed range.
const selectionRange = this._model.schema.getNearestSelectionRange( positionCandidate );
// Remove the old selection range before preparing and adding new selection range. This order is important,
// because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).
const index = this._ranges.indexOf( liveRange );
this._ranges.splice( index, 1 );
liveRange.detach();
// If nearest valid selection range has been found - add it in the place of old range.
// If range is equal to any other selection ranges then it is probably due to contents
// of a multi-range selection being removed. See ckeditor/ckeditor5#6501.
if ( selectionRange && !isRangeCollidingWithSelection( selectionRange, this ) ) {
if ( selectionRange ) {
// Check the range, convert it to live range, bind events, etc.
const newRange = this._prepareRange( selectionRange );
// Add new range in the place of old range.
this._ranges.splice( index, 0, newRange );
this._pushRange( selectionRange );
}
// If nearest valid selection range cannot be found or is intersecting with other selection ranges removing the old range is fine.
// If nearest valid selection range cannot be found don't add any range. Selection will be set to the default range.
}

@@ -1199,6 +1186,1 @@ }

}
// Checks if range collides with any of selection ranges.
function isRangeCollidingWithSelection( range, selection ) {
return !selection._ranges.every( selectionRange => !range.isEqual( selectionRange ) );
}

@@ -107,10 +107,9 @@ /**

*
* element.is( 'image' ); // -> true if this is an <image> element
* element.is( 'element', 'image' ); // -> true if this is an <image> element
* element.is( 'element', 'image' ); // -> same as above
* text.is( 'image' ); -> false
* 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 when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -122,3 +121,2 @@ * @returns {Boolean}

return type === 'element' || type === 'model:element' ||
type === this.name || type === 'model:' + this.name ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.

@@ -215,2 +213,24 @@ type === 'node' || type === 'model: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.

@@ -365,3 +385,3 @@ *

// @if CK_DEBUG_ENGINE // if ( child.is( 'text' ) ) {
// @if CK_DEBUG_ENGINE // if ( child.is( '$text' ) ) {
// @if CK_DEBUG_ENGINE // const textAttrs = convertMapToTags( child._attrs );

@@ -368,0 +388,0 @@

@@ -95,2 +95,12 @@ /**

const markerName = markerOrName instanceof Marker ? markerOrName.name : markerOrName;
if ( markerName.includes( ',' ) ) {
/**
* Marker name cannot contain the "," character.
*
* @error markercollection-incorrect-marker-name
*/
throw new CKEditorError( 'markercollection-incorrect-marker-name: Marker name cannot contain "," character.', this );
}
const oldMarker = this._markers.get( markerName );

@@ -97,0 +107,0 @@

@@ -576,3 +576,3 @@ /**

for ( const item of range.getItems() ) {
if ( item.is( 'textProxy' ) ) {
if ( item.is( '$textProxy' ) ) {
if ( !ignoreWhitespaces ) {

@@ -579,0 +579,0 @@ return true;

@@ -409,3 +409,3 @@ /**

*
* imageElement.is( 'image' ); // -> true
* imageElement.is( 'element', 'image' ); // -> true
* imageElement.is( 'element', 'image' ); // -> same as above

@@ -431,3 +431,3 @@ * imageElement.is( 'model:element', 'image' ); // -> same as above, but more precise

* @method #is
* @param {String} type
* @param {String} type Type to check.
* @returns {Boolean}

@@ -434,0 +434,0 @@ */

@@ -146,3 +146,3 @@ /**

// So, we can operate on those text proxies' text nodes.
const node = item.is( 'textProxy' ) ? item.textNode : item;
const node = item.is( '$textProxy' ) ? item.textNode : item;

@@ -223,3 +223,3 @@ if ( value !== null ) {

// Check if both of those nodes are text objects with same attributes.
if ( nodeBefore && nodeAfter && nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
if ( nodeBefore && nodeAfter && nodeBefore.is( '$text' ) && nodeAfter.is( '$text' ) && _haveSameAttributes( nodeBefore, nodeAfter ) ) {
// Append text of text node after index to the before one.

@@ -226,0 +226,0 @@ const mergedNode = new Text( nodeBefore.data + nodeAfter.data, nodeBefore.getAttributes() );

@@ -149,5 +149,2 @@ /**

/**
* @param {Number} newOffset
*/
set offset( newOffset ) {

@@ -180,3 +177,3 @@ this.path[ this.path.length - 1 ] = newOffset;

if ( parent.is( 'text' ) ) {
if ( parent.is( '$text' ) ) {
/**

@@ -360,2 +357,18 @@ * The position's path is incorrect. This means that a position does not point to

/**
* 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}

@@ -1096,3 +1109,3 @@ * of these two paths must be identical.

if ( node && node.is( 'text' ) && node.startOffset < position.offset ) {
if ( node && node.is( '$text' ) && node.startOffset < position.offset ) {
return node;

@@ -1099,0 +1112,0 @@ }

@@ -280,2 +280,59 @@ /**

/**
* 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.

@@ -282,0 +339,0 @@ *

@@ -71,10 +71,7 @@ /**

*
* rootElement.is( '$root' ); // -> true if this is a $root element
* rootElement.is( 'rootElement', '$root' ); // -> same as above
* text.is( '$root' ); -> 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 when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -88,3 +85,2 @@ * @returns {Boolean}

type === 'element' || type === 'model:element' ||
type === this.name || type === 'model:' + this.name ||
type === 'node' || type === 'model:node';

@@ -91,0 +87,0 @@ }

@@ -189,3 +189,3 @@ /**

itemName = item;
} else if ( item.is && ( item.is( 'text' ) || item.is( 'textProxy' ) ) ) {
} else if ( item.is && ( item.is( '$text' ) || item.is( '$textProxy' ) ) ) {
itemName = '$text';

@@ -224,3 +224,3 @@ }

*
* See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive}
* See the {@glink framework/guides/deep-dive/schema#block-elements Block elements} section of the Schema deep dive
* guide for more details.

@@ -250,3 +250,3 @@ *

*
* See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive}
* See the {@glink framework/guides/deep-dive/schema#limit-elements Limit elements} section of the Schema deep dive
* guide for more details.

@@ -280,3 +280,3 @@ *

*
* See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive}
* See the {@glink framework/guides/deep-dive/schema#object-elements Object elements} section of the Schema deep dive
* guide for more details.

@@ -302,3 +302,3 @@ *

*
* See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive}
* See the {@glink framework/guides/deep-dive/schema#inline-elements Inline elements} section of the Schema deep dive
* guide for more details.

@@ -782,3 +782,3 @@ *

// When node is a `Text` it has no children, so just filter it out.
if ( node.is( 'text' ) ) {
if ( node.is( '$text' ) ) {
removeDisallowedAttributeFromNode( this, node, writer );

@@ -785,0 +785,0 @@ }

@@ -70,8 +70,8 @@ /**

*
* text.is( 'text' ); // -> true
* text.is( '$text' ); // -> true
* text.is( 'node' ); // -> true
* text.is( 'model:text' ); // -> true
* text.is( 'model:$text' ); // -> true
* text.is( 'model:node' ); // -> true
*
* text.is( 'view:text' ); // -> false
* text.is( 'view:$text' ); // -> false
* text.is( 'documentSelection' ); // -> false

@@ -81,8 +81,12 @@ *

*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* **Note:** Until version 20.0.0 this method wasn't accepting `'$text'` type. The legacy `'text'` type is still
* accepted for backward compatibility.
*
* @param {String} type Type to check.
* @returns {Boolean}
*/
is( type ) {
return type === 'text' || type === 'model:text' ||
return type === '$text' || type === 'model:$text' ||
// This are legacy values kept for backward compatibility.
type === 'text' || type === 'model:text' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.

@@ -89,0 +93,0 @@ type === 'node' || type === 'model:node';

@@ -169,6 +169,6 @@ /**

*
* textProxy.is( 'textProxy' ); // -> true
* textProxy.is( 'model:textProxy' ); // -> true
* textProxy.is( '$textProxy' ); // -> true
* textProxy.is( 'model:$textProxy' ); // -> true
*
* textProxy.is( 'view:textProxy' ); // -> false
* textProxy.is( 'view:$textProxy' ); // -> false
* textProxy.is( 'range' ); // -> false

@@ -178,7 +178,12 @@ *

*
* @param {String} type
* **Note:** Until version 20.0.0 this method wasn't accepting `'$textProxy'` type. The legacy `'textProxt'` type is still
* accepted for backward compatibility.
*
* @param {String} type Type to check.
* @returns {Boolean}
*/
is( type ) {
return type === 'textProxy' || type === 'model:textProxy';
return type === '$textProxy' || type === 'model:$textProxy' ||
// This are legacy values kept for backward compatibility.
type === 'textProxy' || type === 'model:textProxy';
}

@@ -185,0 +190,0 @@

@@ -413,3 +413,3 @@ /**

*
* @typedef {'forward'|'backward'} module:engine/view/treewalker~TreeWalkerDirection
* @typedef {'forward'|'backward'} module:engine/model/treewalker~TreeWalkerDirection
*/

@@ -73,3 +73,3 @@ /**

for ( const item of flatSubtreeRange.getItems( { shallow: true } ) ) {
if ( item.is( 'textProxy' ) ) {
if ( item.is( '$textProxy' ) ) {
writer.appendText( item.data, item.getAttributes(), frag );

@@ -76,0 +76,0 @@ } else {

@@ -176,3 +176,3 @@ /**

// Scan only text nodes. Ignore inline elements (like `<softBreak>`).
if ( nextNode && nextNode.is( 'text' ) ) {
if ( nextNode && nextNode.is( '$text' ) ) {
// Check boundary char of an adjacent text node.

@@ -179,0 +179,0 @@ const boundaryChar = nextNode.data.charAt( isForward ? 0 : nextNode.data.length - 1 );

@@ -148,10 +148,9 @@ /**

*
* attributeElement.is( 'b' ); // -> true if this is a bold element
* attributeElement.is( 'element', 'b' ); // -> true if this is a bold element
* attributeElement.is( 'attributeElement', 'b' ); // -> same as above
* text.is( 'b' ); -> false
* text.is( 'element', 'b' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -164,3 +163,2 @@ * @returns {Boolean}

// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||

@@ -167,0 +165,0 @@ type === 'node' || type === 'view:node';

@@ -74,10 +74,9 @@ /**

*
* containerElement.is( 'div' ); // -> true if this is a div container element
* containerElement.is( 'element', 'div' ); // -> true if this is a div container element
* containerElement.is( 'contaienrElement', 'div' ); // -> same as above
* text.is( 'div' ); -> false
* text.is( 'element', 'div' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -90,3 +89,2 @@ * @returns {Boolean}

// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||

@@ -93,0 +91,0 @@ type === 'node' || type === 'view:node';

@@ -227,3 +227,3 @@ /**

// @if CK_DEBUG_ENGINE // for ( const child of this.getChildren() ) {
// @if CK_DEBUG_ENGINE // if ( child.is( 'text' ) ) {
// @if CK_DEBUG_ENGINE // if ( child.is( '$text' ) ) {
// @if CK_DEBUG_ENGINE // string += '\n' + '\t'.repeat( 1 ) + child.data;

@@ -230,0 +230,0 @@ // @if CK_DEBUG_ENGINE // } else {

@@ -32,12 +32,12 @@ /**

/**
* DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
* {@link module:engine/view/domconverter~DomConverter#bindElements binding} these nodes.
* `DomConverter` is a set of tools to do transformations between DOM nodes and view nodes. It also handles
* {@link module:engine/view/domconverter~DomConverter#bindElements bindings} between these nodes.
*
* The instance of DOMConverter is available in {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
* The instance of `DOMConverter` is available under {@link module:engine/view/view~View#domConverter `editor.editing.view.domConverter`}.
*
* DomConverter does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep a
* `DomConverter` does not check which nodes should be rendered (use {@link module:engine/view/renderer~Renderer}), does not keep a
* state of a tree nor keeps synchronization between tree view and DOM tree (use {@link module:engine/view/document~Document}).
*
* DomConverter keeps DOM elements to View element bindings, so when the converter will be destroyed, the binding will
* be lost. Two converters will keep separate binding maps, so one tree view can be bound with two DOM trees.
* `DomConverter` keeps DOM elements to View element bindings, so when the converter gets destroyed, the bindings are lost.
* Two converters will keep separate binding maps, so one tree view can be bound with two DOM trees.
*/

@@ -86,3 +86,3 @@ export default class DomConverter {

*/
this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dd', 'dt', 'figcaption' ];
this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dd', 'dt', 'figcaption', 'td', 'th' ];

@@ -206,3 +206,3 @@ /**

viewToDom( viewNode, domDocument, options = {} ) {
if ( viewNode.is( 'text' ) ) {
if ( viewNode.is( '$text' ) ) {
const textData = this._processDataFromViewText( viewNode );

@@ -242,2 +242,8 @@

// RawElement take care of their children in RawElement#render() method which can be customized
// (see https://github.com/ckeditor/ckeditor5/issues/4469).
if ( viewNode.is( 'rawElement' ) ) {
viewNode.render( domElement );
}
if ( options.bind ) {

@@ -324,3 +330,3 @@ this.bindElements( domElement, viewNode );

if ( viewParent.is( 'text' ) ) {
if ( viewParent.is( '$text' ) ) {
const domParent = this.findCorrespondingDomText( viewParent );

@@ -356,3 +362,3 @@

domBefore = nodeBefore.is( 'text' ) ?
domBefore = nodeBefore.is( '$text' ) ?
this.findCorrespondingDomText( nodeBefore ) :

@@ -401,7 +407,7 @@ this.mapViewToDom( viewPosition.nodeBefore );

// When node is inside UIElement return that UIElement as it's view representation.
const uiElement = this.getParentUIElement( domNode, this._domToViewMapping );
// When node is inside a UIElement or a RawElement return that parent as it's view representation.
const hostElement = this.getHostViewElement( domNode, this._domToViewMapping );
if ( uiElement ) {
return uiElement;
if ( hostElement ) {
return hostElement;
}

@@ -560,6 +566,6 @@

// If position is somewhere inside UIElement - return position before that element.
// If position is somewhere inside UIElement or a RawElement - return position before that element.
const viewElement = this.mapDomToView( domParent );
if ( viewElement && viewElement.is( 'uiElement' ) ) {
if ( viewElement && ( viewElement.is( 'uiElement' ) || viewElement.is( 'rawElement' ) ) ) {
return ViewPosition._createBefore( viewElement );

@@ -616,4 +622,6 @@ }

* to the given DOM - `undefined` is returned.
* For all DOM elements rendered by {@link module:engine/view/uielement~UIElement} that UIElement will be returned.
*
* For all DOM elements rendered by a {@link module:engine/view/uielement~UIElement} or
* a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
*
* @param {DocumentFragment|Element} domElementOrDocumentFragment DOM element or document fragment.

@@ -624,3 +632,5 @@ * @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|undefined}

mapDomToView( domElementOrDocumentFragment ) {
return this.getParentUIElement( domElementOrDocumentFragment ) || this._domToViewMapping.get( domElementOrDocumentFragment );
const hostElement = this.getHostViewElement( domElementOrDocumentFragment );
return hostElement || this._domToViewMapping.get( domElementOrDocumentFragment );
}

@@ -638,3 +648,4 @@

*
* For all text nodes rendered by {@link module:engine/view/uielement~UIElement} that UIElement will be returned.
* For all text nodes rendered by a {@link module:engine/view/uielement~UIElement} or
* a {@link module:engine/view/rawelement~RawElement}, the parent `UIElement` or `RawElement` will be returned.
*

@@ -654,7 +665,7 @@ * Otherwise `null` is returned.

// If DOM text was rendered by UIElement - return that element.
const uiElement = this.getParentUIElement( domText );
// If DOM text was rendered by a UIElement or a RawElement - return this parent element.
const hostElement = this.getHostViewElement( domText );
if ( uiElement ) {
return uiElement;
if ( hostElement ) {
return hostElement;
}

@@ -873,9 +884,9 @@

/**
* Returns parent {@link module:engine/view/uielement~UIElement} for provided DOM node. Returns `null` if there is no
* parent UIElement.
* Returns a parent {@link module:engine/view/uielement~UIElement} or {@link module:engine/view/rawelement~RawElement}
* that hosts the provided DOM node. Returns `null` if there is no such parent.
*
* @param {Node} domNode
* @returns {module:engine/view/uielement~UIElement|null}
* @returns {module:engine/view/uielement~UIElement|module:engine/view/rawelement~RawElement|null}
*/
getParentUIElement( domNode ) {
getHostViewElement( domNode ) {
const ancestors = getAncestors( domNode );

@@ -890,3 +901,3 @@

if ( viewNode && viewNode.is( 'uiElement' ) ) {
if ( viewNode && ( viewNode.is( 'uiElement' ) || viewNode.is( 'rawElement' ) ) ) {
return viewNode;

@@ -903,4 +914,6 @@ }

* The following places are considered as incorrect for selection boundaries:
*
* * before or in the middle of the inline filler sequence,
* * inside the DOM element which represents {@link module:engine/view/uielement~UIElement a view ui element}.
* * inside a DOM element which represents {@link module:engine/view/uielement~UIElement a view UI element},
* * inside a DOM element which represents {@link module:engine/view/rawelement~RawElement a view raw element}.
*

@@ -937,5 +950,6 @@ * @param {Selection} domSelection DOM Selection object to be checked.

// If selection is in `view.UIElement`, it is incorrect. Note that `mapDomToView()` returns `view.UIElement`
// also for any dom element that is inside the view ui element (so we don't need to perform any additional checks).
if ( viewParent && viewParent.is( 'uiElement' ) ) {
// The position is incorrect when anchored inside a UIElement or a RawElement.
// Note: In case of UIElement and RawElement, mapDomToView() returns a parent element for any DOM child
// so there's no need to perform any additional checks.
if ( viewParent && ( viewParent.is( 'uiElement' ) || viewParent.is( 'rawElement' ) ) ) {
return false;

@@ -1154,7 +1168,7 @@ }

// <br> found – it works like a block boundary, so do not scan further.
else if ( value.item.is( 'br' ) ) {
else if ( value.item.is( 'element', 'br' ) ) {
return null;
}
// Found a text node in the same container element.
else if ( value.item.is( 'textProxy' ) ) {
else if ( value.item.is( '$textProxy' ) ) {
return value.item;

@@ -1161,0 +1175,0 @@ }

@@ -86,10 +86,9 @@ /**

*
* editableElement.is( 'div' ); // -> true if this is a div element
* editableElement.is( 'element', 'div' ); // -> true if this is a div element
* editableElement.is( 'editableElement', 'div' ); // -> same as above
* text.is( 'div' ); -> false
* text.is( 'element', 'div' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -103,3 +102,2 @@ * @returns {Boolean}

type === 'containerElement' || type === 'view:containerElement' ||
type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||

@@ -106,0 +104,0 @@ type === 'node' || type === 'view:node';

@@ -167,10 +167,8 @@ /**

*
* element.is( 'img' ); // -> true if this is an <img> element
* element.is( 'element', 'img' ); // -> same as above
* text.is( 'img' ); -> false
* element.is( 'element', 'img' ); // -> true if this is an <img> element
* text.is( 'element', 'img' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -181,4 +179,3 @@ * @returns {Boolean}

if ( !name ) {
return type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||
return type === 'element' || type === 'view:element' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.

@@ -839,3 +836,3 @@ type === 'node' || type === 'view:node';

// @if CK_DEBUG_ENGINE // for ( const child of this.getChildren() ) {
// @if CK_DEBUG_ENGINE // if ( child.is( 'text' ) ) {
// @if CK_DEBUG_ENGINE // if ( child.is( '$text' ) ) {
// @if CK_DEBUG_ENGINE // string += '\n' + '\t'.repeat( level + 1 ) + child.data;

@@ -842,0 +839,0 @@ // @if CK_DEBUG_ENGINE // } else {

@@ -65,10 +65,9 @@ /**

*
* emptyElement.is( 'img' ); // -> true if this is a img element
* emptyElement.is( 'element', 'img' ); // -> true if this is a img element
* emptyElement.is( 'emptyElement', 'img' ); // -> same as above
* text.is( 'img' ); -> false
* text.is( 'element', 'img' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -81,3 +80,2 @@ * @returns {Boolean}

// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||

@@ -84,0 +82,0 @@ type === 'node' || type === 'view:node';

@@ -67,2 +67,4 @@ /**

* Inline filler which is a sequence of the zero width spaces.
*
* @type {String}
*/

@@ -69,0 +71,0 @@ export const INLINE_FILLER = ( () => {

@@ -315,4 +315,3 @@ /**

*
* imgElement.is( 'img' ); // -> true
* imgElement.is( 'element', 'img' ); // -> same as above
* imgElement.is( 'element', 'img' ); // -> true
* imgElement.is( 'view:element', 'img' ); // -> same as above, but more precise

@@ -339,3 +338,3 @@ *

* @method #is
* @param {String} type
* @param {String} type Type to check.
* @returns {Boolean}

@@ -342,0 +341,0 @@ */

@@ -153,4 +153,4 @@ /**

// Do not collect mutations from UIElements.
if ( element && element.is( 'uiElement' ) ) {
// Do not collect mutations from UIElements and RawElements.
if ( element && ( element.is( 'uiElement' ) || element.is( 'rawElement' ) ) ) {
continue;

@@ -169,4 +169,4 @@ }

// Do not collect mutations from UIElements.
if ( element && element.is( 'uiElement' ) ) {
// Do not collect mutations from UIElements and RawElements.
if ( element && ( element.is( 'uiElement' ) || element.is( 'rawElement' ) ) ) {
continue;

@@ -269,3 +269,3 @@ }

// Texts.
else if ( child1.is( 'text' ) && child2.is( 'text' ) ) {
else if ( child1.is( '$text' ) && child2.is( '$text' ) ) {
return child1.data === child2.data;

@@ -272,0 +272,0 @@ }

@@ -62,3 +62,3 @@ /**

get nodeAfter() {
if ( this.parent.is( 'text' ) ) {
if ( this.parent.is( '$text' ) ) {
return null;

@@ -78,3 +78,3 @@ }

get nodeBefore() {
if ( this.parent.is( 'text' ) ) {
if ( this.parent.is( '$text' ) ) {
return null;

@@ -103,3 +103,3 @@ }

get isAtEnd() {
const endOffset = this.parent.is( 'text' ) ? this.parent.data.length : this.parent.childCount;
const endOffset = this.parent.is( '$text' ) ? this.parent.data.length : this.parent.childCount;

@@ -352,3 +352,3 @@ return this.offset === endOffset;

if ( offset == 'end' ) {
offset = node.is( 'text' ) ? node.data.length : node.childCount;
offset = node.is( '$text' ) ? node.data.length : node.childCount;
} else if ( offset == 'before' ) {

@@ -385,3 +385,3 @@ return this._createBefore( node );

// TextProxy is not a instance of Node so we need do handle it in specific way.
if ( item.is( 'textProxy' ) ) {
if ( item.is( '$textProxy' ) ) {
return new Position( item.textNode, item.offsetInText + item.data.length );

@@ -412,3 +412,3 @@ }

// TextProxy is not a instance of Node so we need do handle it in specific way.
if ( item.is( 'textProxy' ) ) {
if ( item.is( '$textProxy' ) ) {
return new Position( item.textNode, item.offsetInText );

@@ -415,0 +415,0 @@ }

@@ -116,7 +116,7 @@ /**

// Fix positions, in case if they are in Text node.
if ( start.parent.is( 'text' ) && start.isAtStart ) {
if ( start.parent.is( '$text' ) && start.isAtStart ) {
start = Position._createBefore( start.parent );
}
if ( end.parent.is( 'text' ) && end.isAtEnd ) {
if ( end.parent.is( '$text' ) && end.isAtEnd ) {
end = Position._createAfter( end.parent );

@@ -157,7 +157,7 @@ }

// Because TreeWalker prefers positions next to text node, we need to move them manually into these text nodes.
if ( nodeAfterStart && nodeAfterStart.is( 'text' ) ) {
if ( nodeAfterStart && nodeAfterStart.is( '$text' ) ) {
start = new Position( nodeAfterStart, 0 );
}
if ( nodeBeforeEnd && nodeBeforeEnd.is( 'text' ) ) {
if ( nodeBeforeEnd && nodeBeforeEnd.is( '$text' ) ) {
end = new Position( nodeBeforeEnd, nodeBeforeEnd.data.length );

@@ -364,7 +364,7 @@ }

//
if ( this.start.parent.is( 'text' ) && this.start.isAtEnd && this.start.parent.nextSibling ) {
if ( this.start.parent.is( '$text' ) && this.start.isAtEnd && this.start.parent.nextSibling ) {
nodeAfterStart = this.start.parent.nextSibling;
}
if ( this.end.parent.is( 'text' ) && this.end.isAtStart && this.end.parent.previousSibling ) {
if ( this.end.parent.is( '$text' ) && this.end.isAtStart && this.end.parent.previousSibling ) {
nodeBeforeEnd = this.end.parent.previousSibling;

@@ -523,3 +523,3 @@ }

static _createOn( item ) {
const size = item.is( 'textProxy' ) ? item.offsetSize : 1;
const size = item.is( '$textProxy' ) ? item.offsetSize : 1;

@@ -526,0 +526,0 @@ return this._createFromPositionAndShift( Position._createBefore( item ), size );

@@ -276,6 +276,6 @@ /**

// The 'uiElement' is a special one and its children are not stored in a view (#799),
// so we cannot use it with replacing flow (since it uses view children during rendering
// which will always result in rendering empty element).
if ( viewChild && !viewChild.is( 'uiElement' ) ) {
// UIElement and RawElement are special cases. Their children are not stored in a view (#799)
// so we cannot use them with replacing flow (since they use view children during rendering
// which will always result in rendering empty elements).
if ( viewChild && !( viewChild.is( 'uiElement' ) || viewChild.is( 'rawElement' ) ) ) {
this._updateElementMappings( viewChild, actualDomChildren[ deleteIndex ] );

@@ -336,3 +336,3 @@ }

if ( firstPos.parent.is( 'text' ) ) {
if ( firstPos.parent.is( '$text' ) ) {
return ViewPosition._createBefore( this.selection.getFirstPosition().parent );

@@ -664,3 +664,3 @@ } else {

if ( viewNode.is( 'text' ) ) {
if ( viewNode.is( '$text' ) ) {
this.markedTexts.add( viewNode );

@@ -667,0 +667,0 @@ } else if ( viewNode.is( 'element' ) ) {

@@ -58,10 +58,9 @@ /**

*
* rootEditableElement.is( 'div' ); // -> true if this is a div root editable element
* rootEditableElement.is( 'element', 'div' ); // -> true if this is a div root editable element
* rootEditableElement.is( 'rootElement', 'div' ); // -> same as above
* text.is( 'div' ); -> false
* text.is( 'element', 'div' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -76,3 +75,2 @@ * @returns {Boolean}

type === 'containerElement' || type === 'view:containerElement' ||
type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||

@@ -79,0 +77,0 @@ type === 'node' || type === 'view:node';

@@ -48,8 +48,8 @@ /**

*
* text.is( 'text' ); // -> true
* text.is( '$text' ); // -> true
* text.is( 'node' ); // -> true
* text.is( 'view:text' ); // -> true
* text.is( 'view:$text' ); // -> true
* text.is( 'view:node' ); // -> true
*
* text.is( 'model:text' ); // -> false
* text.is( 'model:$text' ); // -> false
* text.is( 'element' ); // -> false

@@ -60,7 +60,12 @@ * text.is( 'range' ); // -> false

*
* @param {String} type
* **Note:** Until version 20.0.0 this method wasn't accepting `'$text'` type. The legacy `'text'` type is still
* accepted for backward compatibility.
*
* @param {String} type Type to check.
* @returns {Boolean}
*/
is( type ) {
return type === 'text' || type === 'view:text' ||
return type === '$text' || type === 'view:$text' ||
// This are legacy values kept for backward compatibility.
type === 'text' || type === 'view:text' ||
// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.

@@ -81,4 +86,6 @@ type === 'node' || type === 'view:node';

/**
* This getter is required when using the addition assignment operator on protected property:
* The `_data` property is controlled by a getter and a setter.
*
* The getter is required when using the addition assignment operator on protected property:
*
* const foo = downcastWriter.createText( 'foo' );

@@ -92,2 +99,4 @@ * const bar = downcastWriter.createText( 'bar' );

*
* The setter sets data and fires the {@link module:engine/view/node~Node#event:change:text change event}.
*
* @protected

@@ -100,9 +109,2 @@ * @type {String}

/**
* Sets data and fires the {@link module:engine/view/node~Node#event:change:text change event}.
*
* @protected
* @fires change:text
* @param {String} data New data for the text node.
*/
set _data( data ) {

@@ -109,0 +111,0 @@ this._fireChange( 'text', this );

@@ -146,6 +146,6 @@ /**

*
* textProxy.is( 'textProxy' ); // -> true
* textProxy.is( 'view:textProxy' ); // -> true
* textProxy.is( '$textProxy' ); // -> true
* textProxy.is( 'view:$textProxy' ); // -> true
*
* textProxy.is( 'model:textProxy' ); // -> false
* textProxy.is( 'model:$textProxy' ); // -> false
* textProxy.is( 'element' ); // -> false

@@ -156,7 +156,12 @@ * textProxy.is( 'range' ); // -> false

*
* @param {String} type
* **Note:** Until version 20.0.0 this method wasn't accepting `'$textProxy'` type. The legacy `'textProxy'` type is still
* accepted for backward compatibility.
*
* @param {String} type Type to check.
* @returns {Boolean}
*/
is( type ) {
return type === 'textProxy' || type === 'view:textProxy';
return type === '$textProxy' || type === 'view:$textProxy' ||
// This are legacy values kept for backward compatibility.
type === 'textProxy' || type === 'view:textProxy';
}

@@ -163,0 +168,0 @@

@@ -78,10 +78,9 @@ /**

*
* uiElement.is( 'span' ); // -> true if this is a span ui element
* uiElement.is( 'element', 'span' ); // -> true if this is a span ui element
* uiElement.is( 'uiElement', 'span' ); // -> same as above
* text.is( 'span' ); -> false
* text.is( 'element', 'span' ); -> false
*
* {@link module:engine/view/node~Node#is Check the entire list of view objects} which implement the `is()` method.
*
* @param {String} type Type to check when `name` parameter is present.
* Otherwise, it acts like the `name` parameter.
* @param {String} type Type to check.
* @param {String} [name] Element name.

@@ -94,3 +93,2 @@ * @returns {Boolean}

// From super.is(). This is highly utilised method and cannot call super. See ckeditor/ckeditor5#6529.
type === this.name || type === 'view:' + this.name ||
type === 'element' || type === 'view:element' ||

@@ -97,0 +95,0 @@ type === 'node' || type === 'view:node';

/**
* @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/utils/bindtwostepcarettoattribute
*/
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
/**
* This helper enables the two-step caret (phantom) movement behavior for the given {@link module:engine/model/model~Model}
* attribute on arrow right (<kbd>→</kbd>) and left (<kbd>←</kbd>) key press.
*
* Thanks to this (phantom) caret movement the user is able to type before/after as well as at the
* beginning/end of an attribute.
*
* **Note:** This helper support right–to–left (Arabic, Hebrew, etc.) content by mirroring its behavior
* but for the sake of simplicity examples showcase only left–to–right use–cases.
*
* # Forward movement
*
* ## "Entering" an attribute:
*
* When this behavior is enabled for the `a` attribute and the selection is right before it
* (at the attribute boundary), pressing the right arrow key will not move the selection but update its
* attributes accordingly:
*
* * When enabled:
*
* foo{}<$text a="true">bar</$text>
*
* <kbd>→</kbd>
*
* foo<$text a="true">{}bar</$text>
*
* * When disabled:
*
* foo{}<$text a="true">bar</$text>
*
* <kbd>→</kbd>
*
* foo<$text a="true">b{}ar</$text>
*
*
* ## "Leaving" an attribute:
*
* * When enabled:
*
* <$text a="true">bar{}</$text>baz
*
* <kbd>→</kbd>
*
* <$text a="true">bar</$text>{}baz
*
* * When disabled:
*
* <$text a="true">bar{}</$text>baz
*
* <kbd>→</kbd>
*
* <$text a="true">bar</$text>b{}az
*
* # Backward movement
*
* * When enabled:
*
* <$text a="true">bar</$text>{}baz
*
* <kbd>←</kbd>
*
* <$text a="true">bar{}</$text>baz
*
* * When disabled:
*
* <$text a="true">bar</$text>{}baz
*
* <kbd>←</kbd>
*
* <$text a="true">ba{}r</$text>b{}az
*
* @param {Object} options Helper options.
* @param {module:engine/view/view~View} options.view View controller instance.
* @param {module:engine/model/model~Model} options.model Data model instance.
* @param {module:utils/dom/emittermixin~Emitter} options.emitter The emitter to which this behavior should be added
* (e.g. a plugin instance).
* @param {String} options.attribute Attribute for which this behavior will be added.
* @param {module:utils/locale~Locale} options.locale The {@link module:core/editor/editor~Editor#locale} instance.
*/
export default function bindTwoStepCaretToAttribute( { view, model, emitter, attribute, locale } ) {
const twoStepCaretHandler = new TwoStepCaretHandler( model, emitter, attribute );
const modelSelection = model.document.selection;
// Listen to keyboard events and handle the caret movement according to the 2-step caret logic.
//
// Note: This listener has the "high+1" priority:
// * "high" because of the filler logic implemented in the renderer which also engages on #keydown.
// When the gravity is overridden the attributes of the (model) selection attributes are reset.
// It may end up with the filler kicking in and breaking the selection.
// * "+1" because we would like to avoid collisions with other features (like Widgets), which
// take over the keydown events with the "high" priority. Two-step caret movement takes precedence
// over Widgets in that matter.
//
// Find out more in https://github.com/ckeditor/ckeditor5-engine/issues/1301.
emitter.listenTo( view.document, 'keydown', ( evt, data ) => {
// This implementation works only for collapsed selection.
if ( !modelSelection.isCollapsed ) {
return;
}
// When user tries to expand the selection or jump over the whole word or to the beginning/end then
// two-steps movement is not necessary.
if ( data.shiftKey || data.altKey || data.ctrlKey ) {
return;
}
const arrowRightPressed = data.keyCode == keyCodes.arrowright;
const arrowLeftPressed = data.keyCode == keyCodes.arrowleft;
// When neither left or right arrow has been pressed then do noting.
if ( !arrowRightPressed && !arrowLeftPressed ) {
return;
}
const position = modelSelection.getFirstPosition();
const contentDirection = locale.contentLanguageDirection;
let isMovementHandled;
if ( ( contentDirection === 'ltr' && arrowRightPressed ) || ( contentDirection === 'rtl' && arrowLeftPressed ) ) {
isMovementHandled = twoStepCaretHandler.handleForwardMovement( position, data );
} else {
isMovementHandled = twoStepCaretHandler.handleBackwardMovement( position, data );
}
// Stop the keydown event if the two-step caret movement handled it. Avoid collisions
// with other features which may also take over the caret movement (e.g. Widget).
if ( isMovementHandled ) {
evt.stop();
}
}, { priority: priorities.get( 'high' ) + 1 } );
}
/**
* This is a protected helper–class for {@link module:engine/utils/bindtwostepcarettoattribute}.
* It handles the state of the 2-step caret movement for a single {@link module:engine/model/model~Model}
* attribute upon the `keypress` in the {@link module:engine/view/view~View}.
*
* @protected
*/
export class TwoStepCaretHandler {
/*
* Creates two step handler instance.
*
* @param {module:engine/model/model~Model} model Data model instance.
* @param {module:utils/dom/emittermixin~Emitter} emitter The emitter to which this behavior should be added
* (e.g. a plugin instance).
* @param {String} attribute Attribute for which the behavior will be added.
*/
constructor( model, emitter, attribute ) {
/**
* The model instance this class instance operates on.
*
* @readonly
* @member {module:engine/model/model~Model#schema}
*/
this.model = model;
/**
* The Attribute this class instance operates on.
*
* @readonly
* @member {String}
*/
this.attribute = attribute;
/**
* A reference to the document selection.
*
* @private
* @member {module:engine/model/selection~Selection}
*/
this._modelSelection = model.document.selection;
/**
* The current UID of the overridden gravity, as returned by
* {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
*
* @private
* @member {String}
*/
this._overrideUid = null;
/**
* A flag indicating that the automatic gravity restoration for this attribute
* should not happen upon the next
* {@link module:engine/model/selection~Selection#event:change:range} event.
*
* @private
* @member {String}
*/
this._isNextGravityRestorationSkipped = false;
// The automatic gravity restoration logic.
emitter.listenTo( this._modelSelection, 'change:range', ( evt, data ) => {
// Skipping the automatic restoration is needed if the selection should change
// but the gravity must remain overridden afterwards. See the #handleBackwardMovement
// to learn more.
if ( this._isNextGravityRestorationSkipped ) {
this._isNextGravityRestorationSkipped = false;
return;
}
// Skip automatic restore when the gravity is not overridden — simply, there's nothing to restore
// at this moment.
if ( !this._isGravityOverridden ) {
return;
}
// Skip automatic restore when the change is indirect AND the selection is at the attribute boundary.
// It means that e.g. if the change was external (collaboration) and the user had their
// selection around the link, its gravity should remain intact in this change:range event.
if ( !data.directChange && isAtBoundary( this._modelSelection.getFirstPosition(), attribute ) ) {
return;
}
this._restoreGravity();
} );
}
/**
* Updates the document selection and the view according to the two–step caret movement state
* when moving **forwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
*
* @param {module:engine/model/position~Position} position The model position at the moment of the key press.
* @param {module:engine/view/observer/domeventdata~DomEventData} data Data of the key press.
* @returns {Boolean} `true` when the handler prevented caret movement
*/
handleForwardMovement( position, data ) {
const attribute = this.attribute;
// DON'T ENGAGE 2-SCM if gravity is already overridden. It means that we just entered
//
// <paragraph>foo<$text attribute>{}bar</$text>baz</paragraph>
//
// or left the attribute
//
// <paragraph>foo<$text attribute>bar</$text>{}baz</paragraph>
//
// and the gravity will be restored automatically.
if ( this._isGravityOverridden ) {
return;
}
// DON'T ENGAGE 2-SCM when the selection is at the beginning of the block AND already has the
// attribute:
// * when the selection was initially set there using the mouse,
// * when the editor has just started
//
// <paragraph><$text attribute>{}bar</$text>baz</paragraph>
//
if ( position.isAtStart && this._hasSelectionAttribute ) {
return;
}
// ENGAGE 2-SCM when about to leave one attribute value and enter another:
//
// <paragraph><$text attribute="1">foo{}</$text><$text attribute="2">bar</$text></paragraph>
//
// but DON'T when already in between of them (no attribute selection):
//
// <paragraph><$text attribute="1">foo</$text>{}<$text attribute="2">bar</$text></paragraph>
//
if ( isBetweenDifferentValues( position, attribute ) && this._hasSelectionAttribute ) {
this._preventCaretMovement( data );
this._removeSelectionAttribute();
return true;
}
// ENGAGE 2-SCM when entering an attribute:
//
// <paragraph>foo{}<$text attribute>bar</$text>baz</paragraph>
//
if ( isAtStartBoundary( position, attribute ) ) {
this._preventCaretMovement( data );
this._overrideGravity();
return true;
}
// ENGAGE 2-SCM when leaving an attribute:
//
// <paragraph>foo<$text attribute>bar{}</$text>baz</paragraph>
//
if ( isAtEndBoundary( position, attribute ) && this._hasSelectionAttribute ) {
this._preventCaretMovement( data );
this._overrideGravity();
return true;
}
}
/**
* Updates the document selection and the view according to the two–step caret movement state
* when moving **backwards**. Executed upon `keypress` in the {@link module:engine/view/view~View}.
*
* @param {module:engine/model/position~Position} position The model position at the moment of the key press.
* @param {module:engine/view/observer/domeventdata~DomEventData} data Data of the key press.
* @returns {Boolean} `true` when the handler prevented caret movement
*/
handleBackwardMovement( position, data ) {
const attribute = this.attribute;
// When the gravity is already overridden...
if ( this._isGravityOverridden ) {
// ENGAGE 2-SCM & REMOVE SELECTION ATTRIBUTE
// when about to leave one attribute value and enter another:
//
// <paragraph><$text attribute="1">foo</$text><$text attribute="2">{}bar</$text></paragraph>
//
// but DON'T when already in between of them (no attribute selection):
//
// <paragraph><$text attribute="1">foo</$text>{}<$text attribute="2">bar</$text></paragraph>
//
if ( isBetweenDifferentValues( position, attribute ) && this._hasSelectionAttribute ) {
this._preventCaretMovement( data );
this._restoreGravity();
this._removeSelectionAttribute();
return true;
}
// ENGAGE 2-SCM when at any boundary of the attribute:
//
// <paragraph>foo<$text attribute>bar</$text>{}baz</paragraph>
// <paragraph>foo<$text attribute>{}bar</$text>baz</paragraph>
//
else {
this._preventCaretMovement( data );
this._restoreGravity();
// REMOVE SELECTION ATRIBUTE at the beginning of the block.
// It's like restoring gravity but towards a non-existent content when
// the gravity is overridden:
//
// <paragraph><$text attribute>{}bar</$text></paragraph>
//
// becomes:
//
// <paragraph>{}<$text attribute>bar</$text></paragraph>
//
if ( position.isAtStart ) {
this._removeSelectionAttribute();
}
return true;
}
} else {
// ENGAGE 2-SCM when between two different attribute values but selection has no attribute:
//
// <paragraph><$text attribute="1">foo</$text>{}<$text attribute="2">bar</$text></paragraph>
//
if ( isBetweenDifferentValues( position, attribute ) && !this._hasSelectionAttribute ) {
this._preventCaretMovement( data );
this._setSelectionAttributeFromTheNodeBefore( position );
return true;
}
// End of block boundary cases:
//
// <paragraph><$text attribute>bar{}</$text></paragraph>
// <paragraph><$text attribute>bar</$text>{}</paragraph>
//
if ( position.isAtEnd && isAtEndBoundary( position, attribute ) ) {
// DON'T ENGAGE 2-SCM if the selection has the attribute already.
// This is a common selection if set using the mouse.
//
// <paragraph><$text attribute>bar{}</$text></paragraph>
//
if ( this._hasSelectionAttribute ) {
// DON'T ENGAGE 2-SCM if the attribute at the end of the block which has length == 1.
// Make sure the selection will not the attribute after it moves backwards.
//
// <paragraph>foo<$text attribute>b{}</$text></paragraph>
//
if ( isStepAfterTheAttributeBoundary( position, attribute ) ) {
// Skip the automatic gravity restore upon the next selection#change:range event.
// If not skipped, it would automatically restore the gravity, which should remain
// overridden.
this._skipNextAutomaticGravityRestoration();
this._overrideGravity();
// Don't return "true" here because we didn't call _preventCaretMovement.
// Returning here will destabilize the filler logic, which also listens to
// keydown (and the event would be stopped).
}
return;
}
// ENGAGE 2-SCM if the selection has no attribute. This may happen when the user
// left the attribute using a FORWARD 2-SCM.
//
// <paragraph><$text attribute>bar</$text>{}</paragraph>
//
else {
this._preventCaretMovement( data );
this._setSelectionAttributeFromTheNodeBefore( position );
return true;
}
}
// REMOVE SELECTION ATRIBUTE when restoring gravity towards a non-existent content at the
// beginning of the block.
//
// <paragraph>{}<$text attribute>bar</$text></paragraph>
//
if ( position.isAtStart ) {
if ( this._hasSelectionAttribute ) {
this._removeSelectionAttribute();
this._preventCaretMovement( data );
return true;
}
return;
}
// DON'T ENGAGE 2-SCM when about to enter of leave an attribute.
// We need to check if the caret is a one position before the attribute boundary:
//
// <paragraph>foo<$text attribute>b{}ar</$text>baz</paragraph>
// <paragraph>foo<$text attribute>bar</$text>b{}az</paragraph>
//
if ( isStepAfterTheAttributeBoundary( position, attribute ) ) {
// Skip the automatic gravity restore upon the next selection#change:range event.
// If not skipped, it would automatically restore the gravity, which should remain
// overridden.
this._skipNextAutomaticGravityRestoration();
this._overrideGravity();
// Don't return "true" here because we didn't call _preventCaretMovement.
// Returning here will destabilize the filler logic, which also listens to
// keydown (and the event would be stopped).
}
}
}
/**
* `true` when the gravity is overridden for the {@link #attribute}.
*
* @readonly
* @private
* @type {Boolean}
*/
get _isGravityOverridden() {
return !!this._overrideUid;
}
/**
* `true` when the {@link module:engine/model/selection~Selection} has the {@link #attribute}.
*
* @readonly
* @private
* @type {Boolean}
*/
get _hasSelectionAttribute() {
return this._modelSelection.hasAttribute( this.attribute );
}
/**
* Overrides the gravity using the {@link module:engine/model/writer~Writer model writer}
* and stores the information about this fact in the {@link #_overrideUid}.
*
* A shorthand for {@link module:engine/model/writer~Writer#overrideSelectionGravity}.
*
* @private
*/
_overrideGravity() {
this._overrideUid = this.model.change( writer => writer.overrideSelectionGravity() );
}
/**
* Restores the gravity using the {@link module:engine/model/writer~Writer model writer}.
*
* A shorthand for {@link module:engine/model/writer~Writer#restoreSelectionGravity}.
*
* @private
*/
_restoreGravity() {
this.model.change( writer => {
writer.restoreSelectionGravity( this._overrideUid );
this._overrideUid = null;
} );
}
/**
* Prevents the caret movement in the view by calling `preventDefault` on the event data.
*
* @private
*/
_preventCaretMovement( data ) {
data.preventDefault();
}
/**
* Removes the {@link #attribute} from the selection using using the
* {@link module:engine/model/writer~Writer model writer}.
*
* @private
*/
_removeSelectionAttribute() {
this.model.change( writer => {
writer.removeSelectionAttribute( this.attribute );
} );
}
/**
* Applies the {@link #attribute} to the current selection using using the
* value from the node before the current position. Uses
* the {@link module:engine/model/writer~Writer model writer}.
*
* @private
* @param {module:engine/model/position~Position} position
*/
_setSelectionAttributeFromTheNodeBefore( position ) {
const attribute = this.attribute;
this.model.change( writer => {
writer.setSelectionAttribute( this.attribute, position.nodeBefore.getAttribute( attribute ) );
} );
}
/**
* Skips the next automatic selection gravity restoration upon the
* {@link module:engine/model/selection~Selection#event:change:range} event.
*
* See {@link #_isNextGravityRestorationSkipped}.
*
* @private
*/
_skipNextAutomaticGravityRestoration() {
this._isNextGravityRestorationSkipped = true;
}
}
// @param {module:engine/model/position~Position} position
// @param {String} attribute
// @returns {Boolean} `true` when position between the nodes sticks to the bound of text with given attribute.
function isAtBoundary( position, attribute ) {
return isAtStartBoundary( position, attribute ) || isAtEndBoundary( position, attribute );
}
// @param {module:engine/model/position~Position} position
// @param {String} attribute
function isAtStartBoundary( position, attribute ) {
const { nodeBefore, nodeAfter } = position;
const isAttrBefore = nodeBefore ? nodeBefore.hasAttribute( attribute ) : false;
const isAttrAfter = nodeAfter ? nodeAfter.hasAttribute( attribute ) : false;
return isAttrAfter && ( !isAttrBefore || nodeBefore.getAttribute( attribute ) !== nodeAfter.getAttribute( attribute ) );
}
// @param {module:engine/model/position~Position} position
// @param {String} attribute
function isAtEndBoundary( position, attribute ) {
const { nodeBefore, nodeAfter } = position;
const isAttrBefore = nodeBefore ? nodeBefore.hasAttribute( attribute ) : false;
const isAttrAfter = nodeAfter ? nodeAfter.hasAttribute( attribute ) : false;
return isAttrBefore && ( !isAttrAfter || nodeBefore.getAttribute( attribute ) !== nodeAfter.getAttribute( attribute ) );
}
// @param {module:engine/model/position~Position} position
// @param {String} attribute
function isBetweenDifferentValues( position, attribute ) {
const { nodeBefore, nodeAfter } = position;
const isAttrBefore = nodeBefore ? nodeBefore.hasAttribute( attribute ) : false;
const isAttrAfter = nodeAfter ? nodeAfter.hasAttribute( attribute ) : false;
if ( !isAttrAfter || !isAttrBefore ) {
return;
}
return nodeAfter.getAttribute( attribute ) !== nodeBefore.getAttribute( attribute );
}
// @param {module:engine/model/position~Position} position
// @param {String} attribute
function isStepAfterTheAttributeBoundary( position, attribute ) {
return isAtBoundary( position.getShiftedBy( -1 ), attribute );
}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display