@ckeditor/ckeditor5-engine
Advanced tools
Comparing version 14.0.0 to 15.0.0
{ | ||
"name": "@ckeditor/ckeditor5-engine", | ||
"version": "14.0.0", | ||
"version": "15.0.0", | ||
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.", | ||
@@ -24,20 +24,20 @@ "keywords": [ | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-utils": "^14.0.0", | ||
"@ckeditor/ckeditor5-utils": "^15.0.0", | ||
"lodash-es": "^4.17.10" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-basic-styles": "^11.1.4", | ||
"@ckeditor/ckeditor5-block-quote": "^11.1.3", | ||
"@ckeditor/ckeditor5-core": "^12.3.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^12.1.4", | ||
"@ckeditor/ckeditor5-enter": "^11.1.0", | ||
"@ckeditor/ckeditor5-essentials": "^11.0.5", | ||
"@ckeditor/ckeditor5-heading": "^11.0.5", | ||
"@ckeditor/ckeditor5-link": "^11.1.2", | ||
"@ckeditor/ckeditor5-list": "^12.1.0", | ||
"@ckeditor/ckeditor5-paragraph": "^11.0.5", | ||
"@ckeditor/ckeditor5-theme-lark": "^14.2.0", | ||
"@ckeditor/ckeditor5-typing": "^12.2.0", | ||
"@ckeditor/ckeditor5-undo": "^11.0.5", | ||
"@ckeditor/ckeditor5-widget": "^11.1.0", | ||
"@ckeditor/ckeditor5-basic-styles": "^15.0.0", | ||
"@ckeditor/ckeditor5-block-quote": "^15.0.0", | ||
"@ckeditor/ckeditor5-core": "^15.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^15.0.0", | ||
"@ckeditor/ckeditor5-enter": "^15.0.0", | ||
"@ckeditor/ckeditor5-essentials": "^15.0.0", | ||
"@ckeditor/ckeditor5-heading": "^15.0.0", | ||
"@ckeditor/ckeditor5-link": "^15.0.0", | ||
"@ckeditor/ckeditor5-list": "^15.0.0", | ||
"@ckeditor/ckeditor5-paragraph": "^15.0.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^15.0.0", | ||
"@ckeditor/ckeditor5-typing": "^15.0.0", | ||
"@ckeditor/ckeditor5-undo": "^15.0.0", | ||
"@ckeditor/ckeditor5-widget": "^15.0.0", | ||
"eslint": "^5.5.0", | ||
@@ -44,0 +44,0 @@ "eslint-config-ckeditor5": "^2.0.0", |
CKEditor 5 editing engine | ||
======================================== | ||
[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-engine.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-engine) | ||
@@ -6,0 +5,0 @@ [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-engine.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-engine) |
@@ -408,3 +408,3 @@ /** | ||
const elementName = getViewElementNameFromConfig( config ); | ||
const elementName = getViewElementNameFromConfig( config.view ); | ||
const eventName = elementName ? 'element:' + elementName : 'element'; | ||
@@ -435,3 +435,3 @@ | ||
const elementName = getViewElementNameFromConfig( config ); | ||
const elementName = getViewElementNameFromConfig( config.view ); | ||
const eventName = elementName ? 'element:' + elementName : 'element'; | ||
@@ -498,11 +498,11 @@ | ||
// | ||
// @param {Object} config Conversion config. | ||
// @param {Object} config Conversion view config. | ||
// @returns {String|null} View element name or `null` if name is not directly set. | ||
function getViewElementNameFromConfig( config ) { | ||
if ( typeof config.view == 'string' ) { | ||
return config.view; | ||
function getViewElementNameFromConfig( viewConfig ) { | ||
if ( typeof viewConfig == 'string' ) { | ||
return viewConfig; | ||
} | ||
if ( typeof config.view == 'object' && typeof config.view.name == 'string' ) { | ||
return config.view.name; | ||
if ( typeof viewConfig == 'object' && typeof viewConfig.name == 'string' ) { | ||
return viewConfig.name; | ||
} | ||
@@ -690,3 +690,3 @@ | ||
if ( onlyViewNameIsDefined( config ) ) { | ||
if ( onlyViewNameIsDefined( config.view, data.viewItem ) ) { | ||
match.match.name = true; | ||
@@ -721,10 +721,13 @@ } else { | ||
// | ||
// @param {Object} config Conversion config. | ||
// @param {Object} config Conversion view config. | ||
// @returns {Boolean} | ||
function onlyViewNameIsDefined( config ) { | ||
if ( typeof config.view == 'object' && !getViewElementNameFromConfig( config ) ) { | ||
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 !config.view.classes && !config.view.attributes && !config.view.styles; | ||
return !configToTest.classes && !configToTest.attributes && !configToTest.styles; | ||
} | ||
@@ -731,0 +734,0 @@ |
@@ -14,3 +14,2 @@ /** | ||
import DomConverter from '../view/domconverter'; | ||
import { NBSP_FILLER } from '../view/filler'; | ||
@@ -42,3 +41,3 @@ /** | ||
*/ | ||
this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } ); | ||
this._domConverter = new DomConverter( { blockFillerMode: 'nbsp' } ); | ||
@@ -45,0 +44,0 @@ /** |
@@ -14,3 +14,2 @@ /** | ||
import DomConverter from '../view/domconverter'; | ||
import { NBSP_FILLER } from '../view/filler'; | ||
@@ -58,3 +57,3 @@ /** | ||
*/ | ||
this._domConverter = new DomConverter( { blockFiller: NBSP_FILLER } ); | ||
this._domConverter = new DomConverter( { blockFillerMode: 'nbsp' } ); | ||
@@ -61,0 +60,0 @@ /** |
@@ -236,14 +236,15 @@ /** | ||
/** | ||
* Gets elements of type "block" touched by the 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()` always returns the deepest block. | ||
* **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: | ||
* In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself): | ||
* | ||
* <paragraph>[a</paragraph> | ||
* <quote> | ||
* <blockQuote> | ||
* <paragraph>b</paragraph> | ||
* </quote> | ||
* </blockQuote> | ||
* <paragraph>c]d</paragraph> | ||
@@ -255,2 +256,18 @@ * | ||
* | ||
* In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B: | ||
* | ||
* [<blockA></blockA> | ||
* <blockB> | ||
* <blockC></blockC> | ||
* <blockD></blockD> | ||
* </blockB> | ||
* <blockE></blockE>] | ||
* | ||
* If the selection is inside a block all the inner blocks (A & B) are returned: | ||
* | ||
* <block> | ||
* <blockA>[a</blockA> | ||
* <blockB>b]</blockB> | ||
* </block> | ||
* | ||
* **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective | ||
@@ -270,22 +287,2 @@ * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details. | ||
/** | ||
* Returns blocks that aren't nested in other selected blocks. | ||
* | ||
* In this case the method will return blocks A, B and E because C & D are children of block B: | ||
* | ||
* [<blockA></blockA> | ||
* <blockB> | ||
* <blockC></blockC> | ||
* <blockD></blockD> | ||
* </blockB> | ||
* <blockE></blockE>] | ||
* | ||
* **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}. | ||
* | ||
* @returns {Iterable.<module:engine/model/element~Element>} | ||
*/ | ||
getTopMostBlocks() { | ||
return this._selection.getTopMostBlocks(); | ||
} | ||
/** | ||
* Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only | ||
@@ -292,0 +289,0 @@ * one range in the selection, and that range contains exactly one element. |
@@ -27,2 +27,3 @@ /** | ||
import { injectSelectionPostFixer } from './utils/selection-post-fixer'; | ||
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; | ||
@@ -157,10 +158,14 @@ /** | ||
change( callback ) { | ||
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(), 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(), callback } ); | ||
return this._runPendingChanges()[ 0 ]; | ||
} else { | ||
// If this is not the outermost block, just execute the callback. | ||
return callback( this._currentWriter ); | ||
return this._runPendingChanges()[ 0 ]; | ||
} else { | ||
// If this is not the outermost block, just execute the callback. | ||
return callback( this._currentWriter ); | ||
} | ||
} catch ( err ) { | ||
CKEditorError.rethrowUnexpectedError( err, this ); | ||
} | ||
@@ -203,13 +208,17 @@ } | ||
enqueueChange( batchOrType, callback ) { | ||
if ( typeof batchOrType === 'string' ) { | ||
batchOrType = new Batch( batchOrType ); | ||
} else if ( typeof batchOrType == 'function' ) { | ||
callback = batchOrType; | ||
batchOrType = new Batch(); | ||
} | ||
try { | ||
if ( typeof batchOrType === 'string' ) { | ||
batchOrType = new Batch( batchOrType ); | ||
} else if ( typeof batchOrType == 'function' ) { | ||
callback = batchOrType; | ||
batchOrType = new Batch(); | ||
} | ||
this._pendingChanges.push( { batch: batchOrType, callback } ); | ||
this._pendingChanges.push( { batch: batchOrType, callback } ); | ||
if ( this._pendingChanges.length == 1 ) { | ||
this._runPendingChanges(); | ||
if ( this._pendingChanges.length == 1 ) { | ||
this._runPendingChanges(); | ||
} | ||
} catch ( err ) { | ||
CKEditorError.rethrowUnexpectedError( err, this ); | ||
} | ||
@@ -216,0 +225,0 @@ } |
@@ -736,10 +736,19 @@ /** | ||
for ( const node of nodes ) { | ||
for ( const attribute of node.getAttributeKeys() ) { | ||
if ( !this.checkAttribute( node, attribute ) ) { | ||
writer.removeAttribute( attribute, node ); | ||
} | ||
// 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._createIn( node ); | ||
const positionsInRange = rangeInNode.getPositions(); | ||
if ( node.is( 'element' ) ) { | ||
this.removeDisallowedAttributes( node.getChildren(), writer ); | ||
for ( const position of positionsInRange ) { | ||
const item = position.nodeBefore || position.parent; | ||
removeDisallowedAttributeFromNode( this, item, writer ); | ||
} | ||
} | ||
@@ -1596,1 +1605,9 @@ } | ||
} | ||
function removeDisallowedAttributeFromNode( schema, node, writer ) { | ||
for ( const attribute of node.getAttributeKeys() ) { | ||
if ( !schema.checkAttribute( node, attribute ) ) { | ||
writer.removeAttribute( attribute, node ); | ||
} | ||
} | ||
} |
@@ -644,14 +644,15 @@ /** | ||
/** | ||
* Gets elements of type "block" touched by the 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()` always returns the deepest block. | ||
* **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: | ||
* In this case the function will return exactly all 3 paragraphs (note: `<blockQuote>` is not a block itself): | ||
* | ||
* <paragraph>[a</paragraph> | ||
* <quote> | ||
* <blockQuote> | ||
* <paragraph>b</paragraph> | ||
* </quote> | ||
* </blockQuote> | ||
* <paragraph>c]d</paragraph> | ||
@@ -663,2 +664,18 @@ * | ||
* | ||
* In such a scenario, however, only blocks A, B & E will be returned as blocks C & D are nested in block B: | ||
* | ||
* [<blockA></blockA> | ||
* <blockB> | ||
* <blockC></blockC> | ||
* <blockD></blockD> | ||
* </blockB> | ||
* <blockE></blockE>] | ||
* | ||
* If the selection is inside a block all the inner blocks (A & B) are returned: | ||
* | ||
* <block> | ||
* <blockA>[a</blockA> | ||
* <blockB>b]</blockB> | ||
* </block> | ||
* | ||
* **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective | ||
@@ -677,5 +694,6 @@ * this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details. | ||
for ( const range of this.getRanges() ) { | ||
// Get start block of range in case of a collapsed range. | ||
const startBlock = getParentBlock( range.start, visited ); | ||
if ( startBlock ) { | ||
if ( startBlock && isTopBlockInRange( startBlock, range ) ) { | ||
yield startBlock; | ||
@@ -685,4 +703,6 @@ } | ||
for ( const value of range.getWalker() ) { | ||
if ( value.type == 'elementEnd' && isUnvisitedBlockContainer( value.item, visited ) ) { | ||
yield value.item; | ||
const block = value.item; | ||
if ( value.type == 'elementEnd' && isUnvisitedTopBlock( block, visited, range ) ) { | ||
yield block; | ||
} | ||
@@ -694,3 +714,3 @@ } | ||
// #984. Don't return the end block if the range ends right at its beginning. | ||
if ( endBlock && !range.end.isTouching( Position._createAt( endBlock, 0 ) ) ) { | ||
if ( endBlock && !range.end.isTouching( Position._createAt( endBlock, 0 ) ) && isTopBlockInRange( endBlock, range ) ) { | ||
yield endBlock; | ||
@@ -702,31 +722,2 @@ } | ||
/** | ||
* Returns blocks that aren't nested in other selected blocks. | ||
* | ||
* In this case the method will return blocks A, B and E because C & D are children of block B: | ||
* | ||
* [<blockA></blockA> | ||
* <blockB> | ||
* <blockC></blockC> | ||
* <blockD></blockD> | ||
* </blockB> | ||
* <blockE></blockE>] | ||
* | ||
* **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}. | ||
* | ||
* @returns {Iterable.<module:engine/model/element~Element>} | ||
*/ | ||
* getTopMostBlocks() { | ||
const selected = Array.from( this.getSelectedBlocks() ); | ||
for ( const block of selected ) { | ||
const parentBlock = findAncestorBlock( block ); | ||
// Filter out blocks that are nested in other selected blocks (like paragraphs in tables). | ||
if ( !parentBlock || !selected.includes( parentBlock ) ) { | ||
yield block; | ||
} | ||
} | ||
} | ||
/** | ||
* Checks whether the selection contains the entire content of the given element. This means that selection must start | ||
@@ -840,3 +831,3 @@ * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position | ||
// Marks it as already visited. | ||
function isUnvisitedBlockContainer( element, visited ) { | ||
function isUnvisitedBlock( element, visited ) { | ||
if ( visited.has( element ) ) { | ||
@@ -851,2 +842,7 @@ return false; | ||
// Checks if the given element is a $block was not previously visited and is a top block in a range. | ||
function isUnvisitedTopBlock( element, visited, range ) { | ||
return isUnvisitedBlock( element, visited ) && isTopBlockInRange( element, range ); | ||
} | ||
// Finds the lowest element in position's ancestors which is a block. | ||
@@ -870,3 +866,3 @@ // It will search until first ancestor that is a limit element. | ||
return !hasParentLimit && isUnvisitedBlockContainer( element, visited ); | ||
return !hasParentLimit && isUnvisitedBlock( element, visited ); | ||
} ); | ||
@@ -881,2 +877,19 @@ | ||
// Checks if the blocks is not nested in other block inside a range. | ||
// | ||
// @param {module:engine/model/elmenent~Element} block Block to check. | ||
// @param {module:engine/model/range~Range} range Range to check. | ||
function isTopBlockInRange( block, range ) { | ||
const parentBlock = findAncestorBlock( block ); | ||
if ( !parentBlock ) { | ||
return true; | ||
} | ||
// Add loose flag to check as parentRange can be equal to range. | ||
const isParentInRange = range.containsRange( Range._createOn( parentBlock ), true ); | ||
return !isParentInRange; | ||
} | ||
// Returns first ancestor block of a node. | ||
@@ -883,0 +896,0 @@ // |
@@ -1331,3 +1331,3 @@ /** | ||
/** | ||
* Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()` or | ||
* Trying to use a writer outside a {@link module:engine/model/model~Model#change `change()`} or | ||
* {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`} blocks. | ||
@@ -1334,0 +1334,0 @@ * |
@@ -19,3 +19,3 @@ /** | ||
import ViewTreeWalker from './treewalker'; | ||
import { BR_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, isInlineFiller, startsWithFiller, getDataWithoutFiller } from './filler'; | ||
import { BR_FILLER, getDataWithoutFiller, INLINE_FILLER_LENGTH, isInlineFiller, NBSP_FILLER, startsWithFiller } from './filler'; | ||
@@ -29,2 +29,5 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; | ||
// eslint-disable-next-line new-cap | ||
const BR_FILLER_REF = BR_FILLER( document ); | ||
/** | ||
@@ -47,26 +50,15 @@ * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles | ||
* @param {Object} options Object with configuration options. | ||
* @param {Function} [options.blockFiller=module:engine/view/filler~BR_FILLER] Block filler creator. | ||
* @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode='br'] The type of the block filler to use. | ||
*/ | ||
constructor( options = {} ) { | ||
// Using WeakMap prevent memory leaks: when the converter will be destroyed all referenced between View and DOM | ||
// will be removed. Also because it is a *Weak*Map when both view and DOM elements will be removed referenced | ||
// will be also removed, isn't it brilliant? | ||
// | ||
// Yes, PJ. It is. | ||
// | ||
// You guys so smart. | ||
// | ||
// I've been here. Seen stuff. Afraid of code now. | ||
/** | ||
* Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the | ||
* view to DOM conversion and to recognize block fillers during the DOM to view conversion. | ||
* The mode of a block filler used by DOM converter. | ||
* | ||
* @readonly | ||
* @member {Function} module:engine/view/domconverter~DomConverter#blockFiller | ||
* @member {'br'|'nbsp'} module:engine/view/domconverter~DomConverter#blockFillerMode | ||
*/ | ||
this.blockFiller = options.blockFiller || BR_FILLER; | ||
this.blockFillerMode = options.blockFillerMode || 'br'; | ||
/** | ||
* Tag names of DOM `Element`s which are considered pre-formatted elements. | ||
* Elements which are considered pre-formatted elements. | ||
* | ||
@@ -79,10 +71,25 @@ * @readonly | ||
/** | ||
* Tag names of DOM `Element`s which are considered block elements. | ||
* Elements which are considered block elements (and hence should be filled with a | ||
* {@link #isBlockFiller block filler}). | ||
* | ||
* Whether an element is considered a block element also affects handling of trailing whitespaces. | ||
* | ||
* You can extend this array if you introduce support for block elements which are not yet recognized here. | ||
* | ||
* @readonly | ||
* @member {Array.<String>} module:engine/view/domconverter~DomConverter#blockElements | ||
*/ | ||
this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ]; | ||
this.blockElements = [ 'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dd', 'dt', 'figcaption' ]; | ||
/** | ||
* Block {@link module:engine/view/filler filler} creator, which is used to create all block fillers during the | ||
* view to DOM conversion and to recognize block fillers during the DOM to view conversion. | ||
* | ||
* @readonly | ||
* @private | ||
* @member {Function} module:engine/view/domconverter~DomConverter#_blockFiller | ||
*/ | ||
this._blockFiller = this.blockFillerMode == 'br' ? BR_FILLER : NBSP_FILLER; | ||
/** | ||
* DOM to View mapping. | ||
@@ -113,4 +120,5 @@ * | ||
/** | ||
* Binds given DOM element that represents fake selection to {@link module:engine/view/documentselection~DocumentSelection | ||
* document selection}. Document selection copy is stored and can be retrieved by | ||
* Binds given DOM element that represents fake selection to a **position** of a | ||
* {@link module:engine/view/documentselection~DocumentSelection document selection}. | ||
* Document selection copy is stored and can be retrieved by | ||
* {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} method. | ||
@@ -224,3 +232,7 @@ * | ||
// Create DOM element. | ||
domElement = domDocument.createElement( viewNode.name ); | ||
if ( viewNode.hasAttribute( 'xmlns' ) ) { | ||
domElement = domDocument.createElementNS( viewNode.getAttribute( 'xmlns' ), viewNode.name ); | ||
} else { | ||
domElement = domDocument.createElement( viewNode.name ); | ||
} | ||
@@ -263,3 +275,3 @@ if ( options.bind ) { | ||
if ( fillerPositionOffset === offset ) { | ||
yield this.blockFiller( domDocument ); | ||
yield this._blockFiller( domDocument ); | ||
} | ||
@@ -273,3 +285,3 @@ | ||
if ( fillerPositionOffset === offset ) { | ||
yield this.blockFiller( domDocument ); | ||
yield this._blockFiller( domDocument ); | ||
} | ||
@@ -381,3 +393,3 @@ } | ||
domToView( domNode, options = {} ) { | ||
if ( isBlockFiller( domNode, this.blockFiller ) ) { | ||
if ( this.isBlockFiller( domNode, this.blockFillerMode ) ) { | ||
return null; | ||
@@ -540,3 +552,3 @@ } | ||
domPositionToView( domParent, domOffset ) { | ||
if ( isBlockFiller( domParent, this.blockFiller ) ) { | ||
if ( this.isBlockFiller( domParent, this.blockFillerMode ) ) { | ||
return this.domPositionToView( domParent.parentNode, indexOf( domParent ) ); | ||
@@ -799,2 +811,19 @@ } | ||
/** | ||
* Checks if the node is an instance of the block filler for this DOM converter. | ||
* | ||
* const converter = new DomConverter( { blockFillerMode: 'br' } ); | ||
* | ||
* converter.isBlockFiller( BR_FILLER( document ) ); // true | ||
* converter.isBlockFiller( NBSP_FILLER( document ) ); // false | ||
* | ||
* **Note:**: For the `'nbsp'` mode the method also checks context of a node so it cannot be a detached node. | ||
* | ||
* @param {Node} domNode DOM node to check. | ||
* @returns {Boolean} True if a node is considered a block filler for given mode. | ||
*/ | ||
isBlockFiller( domNode ) { | ||
return this.blockFillerMode == 'br' ? domNode.isEqualNode( BR_FILLER_REF ) : isNbspBlockFiller( domNode, this.blockElements ); | ||
} | ||
/** | ||
* Returns `true` if given selection is a backward selection, that is, if it's `focus` is before `anchor`. | ||
@@ -1210,1 +1239,34 @@ * | ||
} | ||
// Checks if given node is a nbsp block filler. | ||
// | ||
// A is a block filler only if it is a single child of a block element. | ||
// | ||
// @param {Node} domNode DOM node. | ||
// @returns {Boolean} | ||
function isNbspBlockFiller( domNode, blockElements ) { | ||
const isNBSP = isText( domNode ) && domNode.data == '\u00A0'; | ||
return isNBSP && hasBlockParent( domNode, blockElements ) && domNode.parentNode.childNodes.length === 1; | ||
} | ||
// Checks if domNode has block parent. | ||
// | ||
// @param {Node} domNode DOM node. | ||
// @returns {Boolean} | ||
function hasBlockParent( domNode, blockElements ) { | ||
const parent = domNode.parentNode; | ||
return parent && parent.tagName && blockElements.includes( parent.tagName.toLowerCase() ); | ||
} | ||
/** | ||
* Enum representing type of the block filler. | ||
* | ||
* Possible values: | ||
* | ||
* * `br` - for `<br>` block filler used in editing view, | ||
* * `nbsp` - for ` ` block fillers used in the data. | ||
* | ||
* @typedef {String} module:engine/view/filler~BlockFillerMode | ||
*/ |
@@ -6,4 +6,2 @@ /** | ||
/* globals window */ | ||
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; | ||
@@ -41,2 +39,11 @@ import isText from '@ckeditor/ckeditor5-utils/src/dom/istext'; | ||
/** | ||
* Non-breaking space filler creator. This is a function which creates ` ` text node. | ||
* It defines how the filler is created. | ||
* | ||
* @see module:engine/view/filler~BR_FILLER | ||
* @function | ||
*/ | ||
export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' ); | ||
/** | ||
* `<br>` filler creator. This is a function which creates `<br data-cke-filler="true">` element. | ||
@@ -56,11 +63,2 @@ * It defines how the filler is created. | ||
/** | ||
* Non-breaking space filler creator. This is a function which creates ` ` text node. | ||
* It defines how the filler is created. | ||
* | ||
* @see module:engine/view/filler~BR_FILLER | ||
* @function | ||
*/ | ||
export const NBSP_FILLER = domDocument => domDocument.createTextNode( '\u00A0' ); | ||
/** | ||
* Length of the {@link module:engine/view/filler~INLINE_FILLER INLINE_FILLER}. | ||
@@ -71,10 +69,14 @@ */ | ||
/** | ||
* Inline filler which is sequence of the zero width spaces. | ||
* Inline filler which is a sequence of the zero width spaces. | ||
*/ | ||
export let INLINE_FILLER = ''; | ||
export const INLINE_FILLER = ( () => { | ||
let inlineFiller = ''; | ||
for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) { | ||
INLINE_FILLER += '\u200b'; | ||
} | ||
for ( let i = 0; i < INLINE_FILLER_LENGTH; i++ ) { | ||
inlineFiller += '\u200b'; | ||
} | ||
return inlineFiller; | ||
} )(); // Usu IIF so the INLINE_FILLER appears as a constant in the docs. | ||
/** | ||
@@ -126,27 +128,3 @@ * Checks if the node is a text node which starts with the {@link module:engine/view/filler~INLINE_FILLER inline filler}. | ||
// Cache block fillers templates to improve performance. | ||
const templateBlockFillers = new WeakMap(); | ||
/** | ||
* Checks if the node is an instance of the block filler of the given type. | ||
* | ||
* const brFillerInstance = BR_FILLER( document ); | ||
* isBlockFiller( brFillerInstance, BR_FILLER ); // true | ||
* | ||
* @param {Node} domNode DOM node to check. | ||
* @param {Function} blockFiller Block filler creator. | ||
* @returns {Boolean} True if text node contains only {@link module:engine/view/filler~INLINE_FILLER inline filler}. | ||
*/ | ||
export function isBlockFiller( domNode, blockFiller ) { | ||
let templateBlockFiller = templateBlockFillers.get( blockFiller ); | ||
if ( !templateBlockFiller ) { | ||
templateBlockFiller = blockFiller( window.document ); | ||
templateBlockFillers.set( blockFiller, templateBlockFiller ); | ||
} | ||
return domNode.isEqualNode( templateBlockFiller ); | ||
} | ||
/** | ||
* Assign key observer which move cursor from the end of the inline filler to the beginning of it when | ||
@@ -153,0 +131,0 @@ * the left arrow is pressed, so the filler does not break navigation. |
@@ -14,3 +14,3 @@ /** | ||
import ViewPosition from './position'; | ||
import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller, isBlockFiller } from './filler'; | ||
import { INLINE_FILLER, INLINE_FILLER_LENGTH, startsWithFiller, isInlineFiller } from './filler'; | ||
@@ -587,3 +587,3 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; | ||
return diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter.blockFiller ) ); | ||
return diff( actualDomChildren, expectedDomChildren, sameNodes.bind( null, this.domConverter ) ); | ||
} | ||
@@ -696,18 +696,14 @@ | ||
const domDocument = domRoot.ownerDocument; | ||
let container = this._fakeSelectionContainer; | ||
// Create fake selection container if one does not exist. | ||
if ( !container ) { | ||
this._fakeSelectionContainer = container = domDocument.createElement( 'div' ); | ||
if ( !this._fakeSelectionContainer ) { | ||
this._fakeSelectionContainer = createFakeSelectionContainer( domDocument ); | ||
} | ||
Object.assign( container.style, { | ||
position: 'fixed', | ||
top: 0, | ||
left: '-9999px', | ||
// See https://github.com/ckeditor/ckeditor5/issues/752. | ||
width: '42px' | ||
} ); | ||
const container = this._fakeSelectionContainer; | ||
// Fill it with a text node so we can update it later. | ||
container.textContent = '\u00A0'; | ||
// Bind fake selection container with the current selection *position*. | ||
this.domConverter.bindFakeSelection( container, this.selection ); | ||
if ( !this._fakeSelectionNeedsUpdate( domRoot ) ) { | ||
return; | ||
} | ||
@@ -719,6 +715,4 @@ | ||
// Update contents. | ||
container.textContent = this.selection.fakeSelectionLabel || '\u00A0'; | ||
// Update selection. | ||
const domSelection = domDocument.getSelection(); | ||
@@ -730,5 +724,2 @@ const domRange = domDocument.createRange(); | ||
domSelection.addRange( domRange ); | ||
// Bind fake selection container with current selection. | ||
this.domConverter.bindFakeSelection( container, this.selection ); | ||
} | ||
@@ -801,2 +792,27 @@ | ||
/** | ||
* Checks whether the fake selection needs to be updated. | ||
* | ||
* @private | ||
* @param {HTMLElement} domRoot A valid DOM root where a new fake selection container should be added. | ||
* @returns {Boolean} | ||
*/ | ||
_fakeSelectionNeedsUpdate( domRoot ) { | ||
const container = this._fakeSelectionContainer; | ||
const domSelection = domRoot.ownerDocument.getSelection(); | ||
// Fake selection needs to be updated if there's no fake selection container, or the container currently sits | ||
// in a different root. | ||
if ( !container || container.parentElement !== domRoot ) { | ||
return true; | ||
} | ||
// Make sure that the selection actually is within the fake selection. | ||
if ( domSelection.anchorNode !== container && !container.contains( domSelection.anchorNode ) ) { | ||
return true; | ||
} | ||
return container.textContent !== this.selection.fakeSelectionLabel; | ||
} | ||
/** | ||
* Removes the DOM selection. | ||
@@ -919,7 +935,7 @@ * | ||
// @private | ||
// @param {Function} blockFiller Block filler creator function, see {@link module:engine/view/domconverter~DomConverter#blockFiller}. | ||
// @param {String} blockFillerMode Block filler mode, see {@link module:engine/view/domconverter~DomConverter#blockFillerMode}. | ||
// @param {Node} node1 | ||
// @param {Node} node2 | ||
// @returns {Boolean} | ||
function sameNodes( blockFiller, actualDomChild, expectedDomChild ) { | ||
function sameNodes( domConverter, actualDomChild, expectedDomChild ) { | ||
// Elements. | ||
@@ -934,4 +950,4 @@ if ( actualDomChild === expectedDomChild ) { | ||
// Block fillers. | ||
else if ( isBlockFiller( actualDomChild, blockFiller ) && | ||
isBlockFiller( expectedDomChild, blockFiller ) ) { | ||
else if ( domConverter.isBlockFiller( actualDomChild ) && | ||
domConverter.isBlockFiller( expectedDomChild ) ) { | ||
return true; | ||
@@ -984,1 +1000,23 @@ } | ||
} | ||
// Creates a fake selection container for a given document. | ||
// | ||
// @private | ||
// @param {Document} domDocument | ||
// @returns {HTMLElement} | ||
function createFakeSelectionContainer( domDocument ) { | ||
const container = domDocument.createElement( 'div' ); | ||
Object.assign( container.style, { | ||
position: 'fixed', | ||
top: 0, | ||
left: '-9999px', | ||
// See https://github.com/ckeditor/ckeditor5/issues/752. | ||
width: '42px' | ||
} ); | ||
// Fill it with a text node so we can update it later. | ||
container.textContent = '\u00A0'; | ||
return container; | ||
} |
@@ -459,25 +459,29 @@ /** | ||
// Recursive call to view.change() method - execute listener immediately. | ||
if ( this._ongoingChange ) { | ||
return callback( this._writer ); | ||
} | ||
try { | ||
// Recursive call to view.change() method - execute listener immediately. | ||
if ( this._ongoingChange ) { | ||
return callback( this._writer ); | ||
} | ||
// This lock will assure that all recursive calls to view.change() will end up in same block - one "render" | ||
// event for all nested calls. | ||
this._ongoingChange = true; | ||
const callbackResult = callback( this._writer ); | ||
this._ongoingChange = false; | ||
// This lock will assure that all recursive calls to view.change() will end up in same block - one "render" | ||
// event for all nested calls. | ||
this._ongoingChange = true; | ||
const callbackResult = callback( this._writer ); | ||
this._ongoingChange = false; | ||
// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call | ||
// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all changes. | ||
// Also, we don't need to render anything if there're no changes since last rendering. | ||
if ( !this._renderingDisabled && this._hasChangedSinceTheLastRendering ) { | ||
this._postFixersInProgress = true; | ||
this.document._callPostFixers( this._writer ); | ||
this._postFixersInProgress = false; | ||
// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call | ||
// view.change() inside model.change() block - this will ensures that postfixers and rendering are called once after all | ||
// changes. Also, we don't need to render anything if there're no changes since last rendering. | ||
if ( !this._renderingDisabled && this._hasChangedSinceTheLastRendering ) { | ||
this._postFixersInProgress = true; | ||
this.document._callPostFixers( this._writer ); | ||
this._postFixersInProgress = false; | ||
this.fire( 'render' ); | ||
this.fire( 'render' ); | ||
} | ||
return callbackResult; | ||
} catch ( err ) { | ||
CKEditorError.rethrowUnexpectedError( err, this ); | ||
} | ||
return callbackResult; | ||
} | ||
@@ -484,0 +488,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1632618
36867
34
+ Added@ckeditor/ckeditor5-utils@15.0.0(transitive)
+ Addedckeditor5@15.0.0(transitive)
- Removed@ckeditor/ckeditor5-utils@14.0.0(transitive)
- Removedckeditor5@12.4.0(transitive)