@ckeditor/ckeditor5-engine
Advanced tools
Comparing version 30.0.0 to 31.0.0
{ | ||
"name": "@ckeditor/ckeditor5-engine", | ||
"version": "30.0.0", | ||
"version": "31.0.0", | ||
"description": "The editing engine of CKEditor 5 – the best browser-based rich text editor.", | ||
@@ -26,24 +26,26 @@ "keywords": [ | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-utils": "^30.0.0", | ||
"@ckeditor/ckeditor5-utils": "^31.0.0", | ||
"lodash-es": "^4.17.15" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-basic-styles": "^30.0.0", | ||
"@ckeditor/ckeditor5-block-quote": "^30.0.0", | ||
"@ckeditor/ckeditor5-clipboard": "^30.0.0", | ||
"@ckeditor/ckeditor5-core": "^30.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^30.0.0", | ||
"@ckeditor/ckeditor5-enter": "^30.0.0", | ||
"@ckeditor/ckeditor5-essentials": "^30.0.0", | ||
"@ckeditor/ckeditor5-heading": "^30.0.0", | ||
"@ckeditor/ckeditor5-image": "^30.0.0", | ||
"@ckeditor/ckeditor5-link": "^30.0.0", | ||
"@ckeditor/ckeditor5-list": "^30.0.0", | ||
"@ckeditor/ckeditor5-paragraph": "^30.0.0", | ||
"@ckeditor/ckeditor5-table": "^30.0.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^30.0.0", | ||
"@ckeditor/ckeditor5-typing": "^30.0.0", | ||
"@ckeditor/ckeditor5-ui": "^30.0.0", | ||
"@ckeditor/ckeditor5-undo": "^30.0.0", | ||
"@ckeditor/ckeditor5-widget": "^30.0.0", | ||
"@ckeditor/ckeditor5-basic-styles": "^31.0.0", | ||
"@ckeditor/ckeditor5-block-quote": "^31.0.0", | ||
"@ckeditor/ckeditor5-clipboard": "^31.0.0", | ||
"@ckeditor/ckeditor5-cloud-services": "^31.0.0", | ||
"@ckeditor/ckeditor5-core": "^31.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^31.0.0", | ||
"@ckeditor/ckeditor5-enter": "^31.0.0", | ||
"@ckeditor/ckeditor5-essentials": "^31.0.0", | ||
"@ckeditor/ckeditor5-heading": "^31.0.0", | ||
"@ckeditor/ckeditor5-image": "^31.0.0", | ||
"@ckeditor/ckeditor5-link": "^31.0.0", | ||
"@ckeditor/ckeditor5-list": "^31.0.0", | ||
"@ckeditor/ckeditor5-mention": "^31.0.0", | ||
"@ckeditor/ckeditor5-paragraph": "^31.0.0", | ||
"@ckeditor/ckeditor5-table": "^31.0.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^31.0.0", | ||
"@ckeditor/ckeditor5-typing": "^31.0.0", | ||
"@ckeditor/ckeditor5-ui": "^31.0.0", | ||
"@ckeditor/ckeditor5-undo": "^31.0.0", | ||
"@ckeditor/ckeditor5-widget": "^31.0.0", | ||
"webpack": "^4.43.0", | ||
@@ -50,0 +52,0 @@ "webpack-cli": "^3.3.11" |
@@ -148,2 +148,3 @@ /** | ||
this.decorate( 'set' ); | ||
this.decorate( 'get' ); | ||
@@ -167,2 +168,3 @@ // Fire the `ready` event when the initialization has completed. Such low-level listener gives possibility | ||
* | ||
* @fires get | ||
* @param {Object} [options] Additional configuration for the retrieved data. `DataController` provides two optional | ||
@@ -528,2 +530,11 @@ * properties: `rootName` and `trim`. Other properties of this object are specified by various editor features. | ||
*/ | ||
/** | ||
* Event fired after {@link #get get() method} has been run. | ||
* | ||
* The `get` event is fired by decorated {@link #get} method. | ||
* See {@link module:utils/observablemixin~ObservableMixin#decorate} for more information and samples. | ||
* | ||
* @event get | ||
*/ | ||
} | ||
@@ -565,3 +576,41 @@ | ||
return result; | ||
// Sort the markers in a stable fashion to ensure that the order that they are | ||
// added to the model's marker collection does not affect how they are | ||
// downcast. One particular use case that we're targeting here is one where | ||
// two markers are adjacent but not overlapping, such as an insertion/deletion | ||
// suggestion pair represting the replacement of a range of text. In this | ||
// case, putting the markers in DOM order causes the first marker's end to be | ||
// serialized right after the second marker's start, while putting the markers | ||
// in reverse DOM order causes it to be right before the second marker's | ||
// start. So, we sort in a way that ensures non-intersecting ranges are in | ||
// reverse DOM order, and intersecting ranges are in something approximating | ||
// reverse DOM order (since reverse DOM order doesn't have a precise meaning | ||
// when working with intersectng ranges). | ||
return result.sort( ( [ n1, r1 ], [ n2, r2 ] ) => { | ||
if ( r1.end.compareWith( r2.start ) !== 'after' ) { | ||
// m1.end <= m2.start -- m1 is entirely <= m2 | ||
return 1; | ||
} else if ( r1.start.compareWith( r2.end ) !== 'before' ) { | ||
// m1.start >= m2.end -- m1 is entirely >= m2 | ||
return -1; | ||
} else { | ||
// they overlap, so use their start positions as the primary sort key and | ||
// end positions as the secondary sort key | ||
switch ( r1.start.compareWith( r2.start ) ) { | ||
case 'before': | ||
return 1; | ||
case 'after': | ||
return -1; | ||
default: | ||
switch ( r1.end.compareWith( r2.end ) ) { | ||
case 'before': | ||
return 1; | ||
case 'after': | ||
return -1; | ||
default: | ||
return n2.localeCompare( n1 ); | ||
} | ||
} | ||
} | ||
} ); | ||
} |
@@ -31,6 +31,5 @@ /** | ||
* | ||
* @private | ||
* @member {DOMParser} | ||
*/ | ||
this._domParser = new DOMParser(); | ||
this.domParser = new DOMParser(); | ||
@@ -40,6 +39,5 @@ /** | ||
* | ||
* @private | ||
* @member {module:engine/view/domconverter~DomConverter} | ||
*/ | ||
this._domConverter = new DomConverter( document, { blockFillerMode: 'nbsp' } ); | ||
this.domConverter = new DomConverter( document, { renderingMode: 'data' } ); | ||
@@ -49,6 +47,5 @@ /** | ||
* | ||
* @private | ||
* @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter} | ||
* @member {module:engine/dataprocessor/htmlwriter~HtmlWriter} | ||
*/ | ||
this._htmlWriter = new BasicHtmlWriter(); | ||
this.htmlWriter = new BasicHtmlWriter(); | ||
} | ||
@@ -65,6 +62,6 @@ | ||
// Convert view DocumentFragment to DOM DocumentFragment. | ||
const domFragment = this._domConverter.viewToDom( viewFragment, document ); | ||
const domFragment = this.domConverter.viewToDom( viewFragment, document ); | ||
// Convert DOM DocumentFragment to HTML output. | ||
return this._htmlWriter.getHtml( domFragment ); | ||
return this.htmlWriter.getHtml( domFragment ); | ||
} | ||
@@ -83,3 +80,3 @@ | ||
// Convert DOM DocumentFragment to view DocumentFragment. | ||
return this._domConverter.domToView( domFragment ); | ||
return this.domConverter.domToView( domFragment ); | ||
} | ||
@@ -98,3 +95,3 @@ | ||
registerRawContentMatcher( pattern ) { | ||
this._domConverter.registerRawContentMatcher( pattern ); | ||
this.domConverter.registerRawContentMatcher( pattern ); | ||
} | ||
@@ -114,3 +111,3 @@ | ||
useFillerType( type ) { | ||
this._domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp'; | ||
this.domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp'; | ||
} | ||
@@ -127,3 +124,3 @@ | ||
_toDom( data ) { | ||
const document = this._domParser.parseFromString( data, 'text/html' ); | ||
const document = this.domParser.parseFromString( data, 'text/html' ); | ||
const fragment = document.createDocumentFragment(); | ||
@@ -130,0 +127,0 @@ |
@@ -29,3 +29,3 @@ /** | ||
* @param {Object} options Configuration options. | ||
* @param {Array<String>} [options.namespaces=[]] A list of namespaces allowed to use in the XML input. | ||
* @param {Array.<String>} [options.namespaces=[]] A list of namespaces allowed to use in the XML input. | ||
*/ | ||
@@ -39,4 +39,3 @@ constructor( document, options = {} ) { | ||
* | ||
* @public | ||
* @member {DOMParser} | ||
* @member {Array.<String>} | ||
*/ | ||
@@ -48,6 +47,5 @@ this.namespaces = options.namespaces || []; | ||
* | ||
* @private | ||
* @member {DOMParser} | ||
*/ | ||
this._domParser = new DOMParser(); | ||
this.domParser = new DOMParser(); | ||
@@ -57,6 +55,5 @@ /** | ||
* | ||
* @private | ||
* @member {module:engine/view/domconverter~DomConverter} | ||
*/ | ||
this._domConverter = new DomConverter( document, { blockFillerMode: 'nbsp' } ); | ||
this.domConverter = new DomConverter( document, { renderingMode: 'data' } ); | ||
@@ -67,6 +64,5 @@ /** | ||
* | ||
* @private | ||
* @member {module:engine/dataprocessor/basichtmlwriter~BasicHtmlWriter} | ||
* @member {module:engine/dataprocessor/htmlwriter~HtmlWriter} | ||
*/ | ||
this._htmlWriter = new BasicHtmlWriter(); | ||
this.htmlWriter = new BasicHtmlWriter(); | ||
} | ||
@@ -83,7 +79,7 @@ | ||
// Convert view DocumentFragment to DOM DocumentFragment. | ||
const domFragment = this._domConverter.viewToDom( viewFragment, document ); | ||
const domFragment = this.domConverter.viewToDom( viewFragment, document ); | ||
// Convert DOM DocumentFragment to XML output. | ||
// There is no need to use dedicated for XML serializing method because BasicHtmlWriter works well in this case. | ||
return this._htmlWriter.getHtml( domFragment ); | ||
return this.htmlWriter.getHtml( domFragment ); | ||
} | ||
@@ -102,3 +98,3 @@ | ||
// Convert DOM DocumentFragment to view DocumentFragment. | ||
return this._domConverter.domToView( domFragment, { keepOriginalCase: true } ); | ||
return this.domConverter.domToView( domFragment, { keepOriginalCase: true } ); | ||
} | ||
@@ -117,3 +113,3 @@ | ||
registerRawContentMatcher( pattern ) { | ||
this._domConverter.registerRawContentMatcher( pattern ); | ||
this.domConverter.registerRawContentMatcher( pattern ); | ||
} | ||
@@ -133,3 +129,3 @@ | ||
useFillerType( type ) { | ||
this._domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp'; | ||
this.domConverter.blockFillerMode = type == 'marked' ? 'markedNbsp' : 'nbsp'; | ||
} | ||
@@ -152,3 +148,3 @@ | ||
const parsedDocument = this._domParser.parseFromString( data, 'text/xml' ); | ||
const parsedDocument = this.domParser.parseFromString( data, 'text/xml' ); | ||
@@ -155,0 +151,0 @@ // Parse validation. |
@@ -42,2 +42,9 @@ /** | ||
}; | ||
// Returns simplified implementation of {@link module:engine/view/domconverter~DomConverter#setContentOf DomConverter.setContentOf} method. | ||
// Used to render UIElement and RawElement. | ||
const domConverterStub = { | ||
setContentOf: ( node, html ) => { | ||
node.innerHTML = html; | ||
} | ||
}; | ||
@@ -63,2 +70,5 @@ /** | ||
* {@link module:engine/view/rawelement~RawElement} will be printed. | ||
* @param {Object} [options.domConverter=null] When set to an actual {@link module:engine/view/domconverter~DomConverter DomConverter} | ||
* instance it lets the conversion go through exactly the same flow the editing view is going, i.e. with view data | ||
* filtering. Otherwise the simple stub is used. | ||
* @returns {String} The stringified data. | ||
@@ -80,3 +90,4 @@ */ | ||
renderRawElements: options.renderRawElements, | ||
ignoreRoot: true | ||
ignoreRoot: true, | ||
domConverter: options.domConverter | ||
}; | ||
@@ -247,2 +258,5 @@ | ||
* {@link module:engine/view/rawelement~RawElement} will be printed. | ||
* @param {Object} [options.domConverter={}] When set to an actual {@link module:engine/view/domconverter~DomConverter DomConverter} | ||
* instance it lets the conversion go through exactly the same flow the editing view is going, i.e. with view data | ||
* filtering. Otherwise the simple stub is used. | ||
* @returns {String} An HTML-like string representing the view. | ||
@@ -637,2 +651,5 @@ */ | ||
* @param {Boolean} [options.renderRawElements=false] When set to `true`, the inner content of each | ||
* @param {Object} [options.domConverter={}] When set to an actual {@link module:engine/view/domconverter~DomConverter DomConverter} | ||
* instance it lets the conversion go through exactly the same flow the editing view is going, i.e. with view data | ||
* filtering. Otherwise the simple stub is used. | ||
* {@link module:engine/view/rawelement~RawElement} will be printed. | ||
@@ -656,2 +673,3 @@ */ | ||
this.renderRawElements = !!options.renderRawElements; | ||
this.domConverter = options.domConverter || domConverterStub; | ||
} | ||
@@ -690,3 +708,3 @@ | ||
if ( ( this.renderUIElements && root.is( 'uiElement' ) ) ) { | ||
callback( root.render( document ).innerHTML ); | ||
callback( root.render( document, this.domConverter ).innerHTML ); | ||
} else if ( this.renderRawElements && root.is( 'rawElement' ) ) { | ||
@@ -696,3 +714,3 @@ // There's no DOM element for "root" to pass to render(). Creating | ||
const rawContentContainer = document.createElement( 'div' ); | ||
root.render( rawContentContainer ); | ||
root.render( rawContentContainer, this.domConverter ); | ||
@@ -699,0 +717,0 @@ callback( rawContentContainer.innerHTML ); |
@@ -131,5 +131,13 @@ /** | ||
// This might be null ie when editor data is empty. | ||
// In such cases there is no need to fix the selection range. | ||
// This might be null ie when editor data is empty or the selection is inside limit element | ||
// that doesn't allow text inside. | ||
// In the first case there is no need to fix the selection range. | ||
// In the second let's go up to the outer selectable element | ||
if ( !nearestSelectionRange ) { | ||
const ancestorObject = originalPosition.getAncestors().reverse().find( item => schema.isObject( item ) ); | ||
if ( ancestorObject ) { | ||
return Range._createOn( ancestorObject ); | ||
} | ||
return null; | ||
@@ -136,0 +144,0 @@ } |
@@ -84,2 +84,14 @@ /** | ||
/** | ||
* `true` while the user is making a selection in the document (e.g. holding the mouse button and moving the cursor). | ||
* When they stop selecting, the property goes back to `false`. | ||
* | ||
* This property is updated by the {@link module:engine/view/observer/selectionobserver~SelectionObserver}. | ||
* | ||
* @readonly | ||
* @observable | ||
* @member {Boolean} module:engine/view/document~Document#isSelecting | ||
*/ | ||
this.set( 'isSelecting', false ); | ||
/** | ||
* True if composition is in progress inside the document. | ||
@@ -86,0 +98,0 @@ * |
@@ -10,3 +10,3 @@ /** | ||
/* globals document, Node, Text */ | ||
/* globals document, Node, NodeFilter, DOMParser, Text */ | ||
@@ -55,3 +55,8 @@ import ViewText from './text'; | ||
* @param {Object} options An object with configuration options. | ||
* @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode='br'] The type of the block filler to use. | ||
* @param {module:engine/view/filler~BlockFillerMode} [options.blockFillerMode] The type of the block filler to use. | ||
* Default value depends on the options.renderingMode: | ||
* 'nbsp' when options.renderingMode == 'data', | ||
* 'br' when options.renderingMode == 'editing'. | ||
* @param {'data'|'editing'} [options.renderingMode='editing'] Whether to leave the View-to-DOM conversion result unchanged | ||
* or improve editing experience by filtering out interactive data. | ||
*/ | ||
@@ -66,2 +71,17 @@ constructor( document, options = {} ) { | ||
/** | ||
* Whether to leave the View-to-DOM conversion result unchanged or improve editing experience by filtering out interactive data. | ||
* | ||
* @member {'data'|'editing'} module:engine/view/domconverter~DomConverter#renderingMode | ||
*/ | ||
this.renderingMode = options.renderingMode || 'editing'; | ||
/** | ||
* Main switch for new rendering approach in the editing view. | ||
* | ||
* @protected | ||
* @member {Boolean} | ||
*/ | ||
this.experimentalRenderingMode = false; | ||
/** | ||
* The mode of a block filler used by the DOM converter. | ||
@@ -71,3 +91,3 @@ * | ||
*/ | ||
this.blockFillerMode = options.blockFillerMode || 'br'; | ||
this.blockFillerMode = options.blockFillerMode || ( this.renderingMode === 'editing' ? 'br' : 'nbsp' ); | ||
@@ -229,2 +249,78 @@ /** | ||
/** | ||
* Decides whether given pair of attribute key and value should be passed further down the pipeline. | ||
* | ||
* @param {String} attributeKey | ||
* @param {String} attributeValue | ||
* @returns {Boolean} | ||
*/ | ||
shouldRenderAttribute( attributeKey, attributeValue ) { | ||
if ( !this.experimentalRenderingMode || this.renderingMode === 'data' ) { | ||
return true; | ||
} | ||
return !( attributeKey.toLowerCase().startsWith( 'on' ) || | ||
attributeValue.match( /(\b)(on\S+)(\s*)=|javascript:|(<\s*)(\/*)script/i ) || | ||
attributeValue.match( /data:(?!image\/(png|jpeg|gif|webp))/i ) | ||
); | ||
} | ||
/** | ||
* Set `domElement`'s content using provided `html` argument. Apply necessary filtering for the editing pipeline. | ||
* | ||
* @param {Element} domElement DOM element that should have `html` set as its content. | ||
* @param {String} html Textual representation of the HTML that will be set on `domElement`. | ||
*/ | ||
setContentOf( domElement, html ) { | ||
// For data pipeline we pass the HTML as-is. | ||
if ( !this.experimentalRenderingMode || this.renderingMode === 'data' ) { | ||
domElement.innerHTML = html; | ||
return; | ||
} | ||
const document = new DOMParser().parseFromString( html, 'text/html' ); | ||
const fragment = document.createDocumentFragment(); | ||
const bodyChildNodes = document.body.childNodes; | ||
while ( bodyChildNodes.length > 0 ) { | ||
fragment.appendChild( bodyChildNodes[ 0 ] ); | ||
} | ||
const treeWalker = document.createTreeWalker( fragment, NodeFilter.SHOW_ELEMENT ); | ||
const nodes = []; | ||
let currentNode; | ||
// eslint-disable-next-line no-cond-assign | ||
while ( currentNode = treeWalker.nextNode() ) { | ||
nodes.push( currentNode ); | ||
} | ||
for ( const currentNode of nodes ) { | ||
// Go through nodes to remove those that are prohibited in editing pipeline. | ||
for ( const attributeName of currentNode.getAttributeNames() ) { | ||
const attributeValue = currentNode.getAttribute( attributeName ); | ||
if ( !this.shouldRenderAttribute( attributeName, attributeValue ) ) { | ||
currentNode.removeAttribute( attributeName ); | ||
} | ||
} | ||
const elementName = currentNode.tagName.toLowerCase(); | ||
// There are certain nodes, that should be renamed to <span> in editing pipeline. | ||
if ( this._shouldRenameElement( elementName ) ) { | ||
currentNode.replaceWith( this._createReplacementDomElement( elementName, currentNode ) ); | ||
} | ||
} | ||
// Empty the target element. | ||
while ( domElement.firstChild ) { | ||
domElement.firstChild.remove(); | ||
} | ||
domElement.append( fragment ); | ||
} | ||
/** | ||
* Converts the view to the DOM. For all text nodes, not bound elements and document fragments new items will | ||
@@ -265,3 +361,3 @@ * be created. For bound elements and document fragments the method will return corresponding items. | ||
// UIElement has its own render() method (see #799). | ||
domElement = viewNode.render( domDocument ); | ||
domElement = viewNode.render( domDocument, this ); | ||
} | ||
@@ -276,3 +372,5 @@ | ||
// Create DOM element. | ||
if ( viewNode.hasAttribute( 'xmlns' ) ) { | ||
if ( this._shouldRenameElement( viewNode.name ) ) { | ||
domElement = this._createReplacementDomElement( viewNode.name ); | ||
} else if ( viewNode.hasAttribute( 'xmlns' ) ) { | ||
domElement = domDocument.createElementNS( viewNode.getAttribute( 'xmlns' ), viewNode.name ); | ||
@@ -286,3 +384,3 @@ } else { | ||
if ( viewNode.is( 'rawElement' ) ) { | ||
viewNode.render( domElement ); | ||
viewNode.render( domElement, this ); | ||
} | ||
@@ -296,3 +394,9 @@ | ||
for ( const key of viewNode.getAttributeKeys() ) { | ||
domElement.setAttribute( key, viewNode.getAttribute( key ) ); | ||
const value = viewNode.getAttribute( key ); | ||
if ( !this.shouldRenderAttribute( key, value ) ) { | ||
continue; | ||
} | ||
domElement.setAttribute( key, value ); | ||
} | ||
@@ -615,6 +719,6 @@ } | ||
* @param {Node} domParent DOM position parent. | ||
* @param {Number} domOffset DOM position offset. | ||
* @param {Number} [domOffset=0] DOM position offset. You can skip it when converting the inline filler node. | ||
* @returns {module:engine/view/position~Position} viewPosition View position. | ||
*/ | ||
domPositionToView( domParent, domOffset ) { | ||
domPositionToView( domParent, domOffset = 0 ) { | ||
if ( this.isBlockFiller( domParent ) ) { | ||
@@ -1386,2 +1490,41 @@ return this.domPositionToView( domParent.parentNode, indexOf( domParent ) ); | ||
} | ||
/** | ||
* Checks whether given element name should be renamed in a current rendering mode. | ||
* | ||
* @private | ||
* @param {String} elementName The name of view element. | ||
* @returns {Boolean} | ||
*/ | ||
_shouldRenameElement( elementName ) { | ||
return this.experimentalRenderingMode && this.renderingMode == 'editing' && elementName == 'script'; | ||
} | ||
/** | ||
* Return a <span> element with special attribute holding the name of the original element. | ||
* Optionally, copy all the attributes of the original element if that element is provided. | ||
* | ||
* @private | ||
* @param {String} elementName The name of view element. | ||
* @param {Element} [originalDomElement] The original DOM element to copy attributes and content from. | ||
* @returns {Element} | ||
*/ | ||
_createReplacementDomElement( elementName, originalDomElement = null ) { | ||
const newDomElement = document.createElement( 'span' ); | ||
// Mark the span replacing a script as hidden. | ||
newDomElement.setAttribute( 'data-ck-hidden', elementName ); | ||
if ( originalDomElement ) { | ||
while ( originalDomElement.firstChild ) { | ||
newDomElement.appendChild( originalDomElement.firstChild ); | ||
} | ||
for ( const attributeName of originalDomElement.getAttributeNames() ) { | ||
newDomElement.setAttribute( attributeName, originalDomElement.getAttribute( attributeName ) ); | ||
} | ||
} | ||
return newDomElement; | ||
} | ||
} | ||
@@ -1388,0 +1531,0 @@ |
@@ -23,2 +23,4 @@ /** | ||
* | ||
* This observer also manages the {@link module:engine/view/document~Document#isSelecting} property of the view document. | ||
* | ||
* Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default. | ||
@@ -82,5 +84,23 @@ * | ||
/** | ||
* When called, starts clearing the {@link #_loopbackCounter} counter in intervals of time. When the number of selection | ||
* changes exceeds a certain limit within the interval of time, the observer will not fire `selectionChange` but warn about | ||
* possible infinite selection loop. | ||
* | ||
* @private | ||
* @member {Number} #_clearInfiniteLoopInterval | ||
*/ | ||
this._clearInfiniteLoopInterval = setInterval( () => this._clearInfiniteLoop(), 1000 ); | ||
/** | ||
* Unlocks the `isSelecting` state of the view document in case the selection observer did not record this fact | ||
* correctly (for whatever the reason). It is a safeguard (paranoid check) that returns document to the normal state | ||
* after a certain period of time (debounced, postponed by each selectionchange event). | ||
* | ||
* @private | ||
* @method #_documentIsSelectingInactivityTimeoutDebounced | ||
*/ | ||
this._documentIsSelectingInactivityTimeoutDebounced = debounce( () => ( this.document.isSelecting = false ), 5000 ); | ||
/** | ||
* Private property to check if the code does not enter infinite loop. | ||
@@ -100,3 +120,24 @@ * | ||
// Add listener once per each document. | ||
const startDocumentIsSelecting = () => { | ||
this.document.isSelecting = true; | ||
// Let's activate the safety timeout each time the document enters the "is selecting" state. | ||
this._documentIsSelectingInactivityTimeoutDebounced(); | ||
}; | ||
const endDocumentIsSelecting = () => { | ||
this.document.isSelecting = false; | ||
// The safety timeout can be canceled when the document leaves the "is selecting" state. | ||
this._documentIsSelectingInactivityTimeoutDebounced.cancel(); | ||
}; | ||
// The document has the "is selecting" state while the user keeps making (extending) the selection | ||
// (e.g. by holding the mouse button and moving the cursor). The state resets when they either released | ||
// the mouse button or interrupted the process by pressing or releasing any key. | ||
this.listenTo( domElement, 'selectstart', startDocumentIsSelecting, { priority: 'highest' } ); | ||
this.listenTo( domElement, 'keydown', endDocumentIsSelecting, { priority: 'highest' } ); | ||
this.listenTo( domElement, 'keyup', endDocumentIsSelecting, { priority: 'highest' } ); | ||
// Add document-wide listeners only once. This method could be called for multiple editing roots. | ||
if ( this._documents.has( domDocument ) ) { | ||
@@ -106,4 +147,9 @@ return; | ||
this.listenTo( domDocument, 'mouseup', endDocumentIsSelecting, { priority: 'highest' } ); | ||
this.listenTo( domDocument, 'selectionchange', ( evt, domEvent ) => { | ||
this._handleSelectionChange( domEvent, domDocument ); | ||
// Defer the safety timeout when the selection changes (e.g. the user keeps extending the selection | ||
// using their mouse). | ||
this._documentIsSelectingInactivityTimeoutDebounced(); | ||
} ); | ||
@@ -122,2 +168,3 @@ | ||
this._fireSelectionChangeDoneDebounced.cancel(); | ||
this._documentIsSelectingInactivityTimeoutDebounced.cancel(); | ||
} | ||
@@ -124,0 +171,0 @@ |
@@ -134,4 +134,4 @@ /** | ||
* | ||
* myRawElement.render = function( domElement ) { | ||
* domElement.innerHTML = '<b>This is the raw content of myRawElement.</b>'; | ||
* myRawElement.render = function( domElement, domConverter ) { | ||
* domConverter.setContentOf( domElement, '<b>This is the raw content of myRawElement.</b>' ); | ||
* }; | ||
@@ -141,2 +141,3 @@ * | ||
* @param {HTMLElement} domElement The native DOM element representing the raw view element. | ||
* @param {module:engine/view/domconverter~DomConverter} domConverter Instance of the DomConverter used to optimize the output. | ||
*/ | ||
@@ -143,0 +144,0 @@ } |
@@ -27,2 +27,4 @@ /** | ||
import '../../theme/renderer.css'; | ||
/** | ||
@@ -102,6 +104,32 @@ * Renderer is responsible for updating the DOM structure and the DOM selection based on | ||
* @member {Boolean} | ||
* @observable | ||
*/ | ||
this.isFocused = false; | ||
this.set( 'isFocused', false ); | ||
/** | ||
* Indicates whether the user is making a selection in the document (e.g. holding the mouse button and moving the cursor). | ||
* When they stop selecting, the property goes back to `false`. | ||
* | ||
* Note: In some browsers, the renderer will stop rendering the selection and inline fillers while the user is making | ||
* a selection to avoid glitches in DOM selection | ||
* (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723). | ||
* | ||
* @member {Boolean} | ||
* @observable | ||
*/ | ||
this.set( 'isSelecting', false ); | ||
// Rendering the selection and inline filler manipulation should be postponed in (non-Android) Blink until the user finishes | ||
// creating the selection in DOM to avoid accidental selection collapsing | ||
// (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723). | ||
// When the user stops, selecting, all pending changes should be rendered ASAP, though. | ||
if ( env.isBlink && !env.isAndroid ) { | ||
this.on( 'change:isSelecting', () => { | ||
if ( !this.isSelecting ) { | ||
this.render(); | ||
} | ||
} ); | ||
} | ||
/** | ||
* The text node in which the inline filler was rendered. | ||
@@ -175,2 +203,3 @@ * | ||
let inlineFillerPosition; | ||
const isInlineFillerRenderingPossible = env.isBlink && !env.isAndroid ? !this.isSelecting : true; | ||
@@ -182,20 +211,31 @@ // Refresh mappings. | ||
// There was inline filler rendered in the DOM but it's not | ||
// at the selection position any more, so we can remove it | ||
// (cause even if it's needed, it must be placed in another location). | ||
if ( this._inlineFiller && !this._isSelectionInInlineFiller() ) { | ||
this._removeInlineFiller(); | ||
} | ||
// Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental | ||
// DOM selection collapsing | ||
// (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723). | ||
if ( isInlineFillerRenderingPossible ) { | ||
// There was inline filler rendered in the DOM but it's not | ||
// at the selection position any more, so we can remove it | ||
// (cause even if it's needed, it must be placed in another location). | ||
if ( this._inlineFiller && !this._isSelectionInInlineFiller() ) { | ||
this._removeInlineFiller(); | ||
} | ||
// If we've got the filler, let's try to guess its position in the view. | ||
if ( this._inlineFiller ) { | ||
inlineFillerPosition = this._getInlineFillerPosition(); | ||
} | ||
// Otherwise, if it's needed, create it at the selection position. | ||
else if ( this._needsInlineFillerAtSelection() ) { | ||
inlineFillerPosition = this.selection.getFirstPosition(); | ||
// If we've got the filler, let's try to guess its position in the view. | ||
if ( this._inlineFiller ) { | ||
inlineFillerPosition = this._getInlineFillerPosition(); | ||
} | ||
// Otherwise, if it's needed, create it at the selection position. | ||
else if ( this._needsInlineFillerAtSelection() ) { | ||
inlineFillerPosition = this.selection.getFirstPosition(); | ||
// Do not use `markToSync` so it will be added even if the parent is already added. | ||
this.markedChildren.add( inlineFillerPosition.parent ); | ||
// Do not use `markToSync` so it will be added even if the parent is already added. | ||
this.markedChildren.add( inlineFillerPosition.parent ); | ||
} | ||
} | ||
// Paranoid check: we make sure the inline filler has any parent so it can be mapped to view position | ||
// by DomConverter. | ||
else if ( this._inlineFiller && this._inlineFiller.parentNode ) { | ||
// While the user is making selection, preserve the inline filler at its original position. | ||
inlineFillerPosition = this.domConverter.domPositionToView( this._inlineFiller ); | ||
} | ||
@@ -216,22 +256,26 @@ for ( const element of this.markedAttributes ) { | ||
// Check whether the inline filler is required and where it really is in the DOM. | ||
// At this point in most cases it will be in the DOM, but there are exceptions. | ||
// For example, if the inline filler was deep in the created DOM structure, it will not be created. | ||
// Similarly, if it was removed at the beginning of this function and then neither text nor children were updated, | ||
// it will not be present. | ||
// Fix those and similar scenarios. | ||
if ( inlineFillerPosition ) { | ||
const fillerDomPosition = this.domConverter.viewPositionToDom( inlineFillerPosition ); | ||
const domDocument = fillerDomPosition.parent.ownerDocument; | ||
// * Check whether the inline filler is required and where it really is in the DOM. | ||
// At this point in most cases it will be in the DOM, but there are exceptions. | ||
// For example, if the inline filler was deep in the created DOM structure, it will not be created. | ||
// Similarly, if it was removed at the beginning of this function and then neither text nor children were updated, | ||
// it will not be present. Fix those and similar scenarios. | ||
// * Don't manipulate inline fillers while the selection is being made in (non-Android) Blink to prevent accidental | ||
// DOM selection collapsing | ||
// (https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723). | ||
if ( isInlineFillerRenderingPossible ) { | ||
if ( inlineFillerPosition ) { | ||
const fillerDomPosition = this.domConverter.viewPositionToDom( inlineFillerPosition ); | ||
const domDocument = fillerDomPosition.parent.ownerDocument; | ||
if ( !startsWithFiller( fillerDomPosition.parent ) ) { | ||
// Filler has not been created at filler position. Create it now. | ||
this._inlineFiller = addInlineFiller( domDocument, fillerDomPosition.parent, fillerDomPosition.offset ); | ||
if ( !startsWithFiller( fillerDomPosition.parent ) ) { | ||
// Filler has not been created at filler position. Create it now. | ||
this._inlineFiller = addInlineFiller( domDocument, fillerDomPosition.parent, fillerDomPosition.offset ); | ||
} else { | ||
// Filler has been found, save it. | ||
this._inlineFiller = fillerDomPosition.parent; | ||
} | ||
} else { | ||
// Filler has been found, save it. | ||
this._inlineFiller = fillerDomPosition.parent; | ||
// There is no filler needed. | ||
this._inlineFiller = null; | ||
} | ||
} else { | ||
// There is no filler needed. | ||
this._inlineFiller = null; | ||
} | ||
@@ -409,3 +453,3 @@ | ||
if ( isInlineFiller( domFillerNode ) ) { | ||
domFillerNode.parentNode.removeChild( domFillerNode ); | ||
domFillerNode.remove(); | ||
} else { | ||
@@ -520,3 +564,9 @@ domFillerNode.data = domFillerNode.data.substr( INLINE_FILLER_LENGTH ); | ||
for ( const key of viewAttrKeys ) { | ||
domElement.setAttribute( key, viewElement.getAttribute( key ) ); | ||
const value = viewElement.getAttribute( key ); | ||
if ( !this.domConverter.shouldRenderAttribute( key, value ) ) { | ||
domElement.removeAttribute( key ); | ||
} else { | ||
domElement.setAttribute( key, value ); | ||
} | ||
} | ||
@@ -526,2 +576,8 @@ | ||
for ( const key of domAttrKeys ) { | ||
// Do not remove attributes on `script` elements with special data attributes `data-ck-hidden`. | ||
if ( viewElement.name === 'script' && key === 'data-ck-hidden' ) { | ||
continue; | ||
} | ||
// All other attributes not present in the DOM should be removed. | ||
if ( !viewElement.hasAttribute( key ) ) { | ||
@@ -554,3 +610,3 @@ domElement.removeAttribute( key ); | ||
const expectedDomChildren = Array.from( | ||
this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { bind: true, inlineFillerPosition } ) | ||
this.domConverter.viewChildrenToDom( viewElement, domElement.ownerDocument, { bind: true } ) | ||
); | ||
@@ -696,2 +752,10 @@ | ||
_updateSelection() { | ||
// Block updating DOM selection in (non-Android) Blink while the user is selecting to prevent accidental selection collapsing. | ||
// Note: Structural changes in DOM must trigger selection rendering, though. Nodes the selection was anchored | ||
// to may disappear in DOM which would break the selection (e.g. in real-time collaboration scenarios). | ||
// https://github.com/ckeditor/ckeditor5/issues/10562, https://github.com/ckeditor/ckeditor5/issues/10723 | ||
if ( env.isBlink && !env.isAndroid && this.isSelecting && !this.markedChildren.size ) { | ||
return; | ||
} | ||
// If there is no selection - remove DOM and fake selections. | ||
@@ -698,0 +762,0 @@ if ( this.selection.rangeCount === 0 ) { |
@@ -129,6 +129,7 @@ /** | ||
* const myUIElement = downcastWriter.createUIElement( 'span' ); | ||
* myUIElement.render = function( domDocument ) { | ||
* myUIElement.render = function( domDocument, domConverter ) { | ||
* const domElement = this.toDomElement( domDocument ); | ||
* domElement.innerHTML = '<b>this is ui element</b>'; | ||
* | ||
* domConverter.setContentOf( domElement, '<b>this is ui element</b>' ); | ||
* | ||
* return domElement; | ||
@@ -142,5 +143,7 @@ * }; | ||
* @param {Document} domDocument | ||
* @param {module:engine/view/domconverter~DomConverter} domConverter Instance of the DomConverter used to optimize the output. | ||
* @returns {HTMLElement} | ||
*/ | ||
render( domDocument ) { | ||
// Provide basic, default output. | ||
return this.toDomElement( domDocument ); | ||
@@ -147,0 +150,0 @@ } |
@@ -119,3 +119,3 @@ /** | ||
this._renderer = new Renderer( this.domConverter, this.document.selection ); | ||
this._renderer.bind( 'isFocused' ).to( this.document ); | ||
this._renderer.bind( 'isFocused', 'isSelecting' ).to( this.document ); | ||
@@ -122,0 +122,0 @@ /** |
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
1750484
118
41750
7
22
+ Added@ckeditor/ckeditor5-utils@31.1.0(transitive)
- Removed@ckeditor/ckeditor5-utils@30.0.0(transitive)