Procházet zdrojové kódy

Made it possible to use the RawElement in the DomConverter.

Aleksander Nowodzinski před 5 roky
rodič
revize
eec1c98e83

+ 40 - 29
packages/ckeditor5-engine/src/view/domconverter.js

@@ -29,16 +29,16 @@ import { isElement } from 'lodash-es';
 const BR_FILLER_REF = BR_FILLER( document );
 
 /**
- * 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.
  */
 export default class DomConverter {
 	/**
@@ -229,8 +229,12 @@ export default class DomConverter {
 
 				return domElement;
 			} else {
+				// RawElement has its own render() method (see https://github.com/ckeditor/ckeditor5/issues/4469).
+				if ( viewNode.is( 'rawElement' ) ) {
+					domElement = viewNode.render( domDocument );
+				}
 				// Create DOM element.
-				if ( viewNode.hasAttribute( 'xmlns' ) ) {
+				else if ( viewNode.hasAttribute( 'xmlns' ) ) {
 					domElement = domDocument.createElementNS( viewNode.getAttribute( 'xmlns' ), viewNode.name );
 				} else {
 					domElement = domDocument.createElement( viewNode.name );
@@ -392,11 +396,11 @@ export default class DomConverter {
 			return null;
 		}
 
-		// 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;
 		}
 
 		if ( isText( domNode ) ) {
@@ -550,10 +554,10 @@ export default class DomConverter {
 			return this.domPositionToView( domParent.parentNode, indexOf( domParent ) );
 		}
 
-		// 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 );
 		}
 
@@ -605,14 +609,18 @@ export default class DomConverter {
 	 * {@link module:engine/view/documentfragment~DocumentFragment} for provided DOM element or
 	 * document fragment. If there is no view item {@link module:engine/view/domconverter~DomConverter#bindElements bound}
 	 * 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.
 	 * @returns {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment|undefined}
 	 * Corresponding view element, document fragment or `undefined` if no element was bound.
 	 */
 	mapDomToView( domElementOrDocumentFragment ) {
-		return this.getParentUIElement( domElementOrDocumentFragment ) || this._domToViewMapping.get( domElementOrDocumentFragment );
+		const hostElement = this.getHostViewElement( domElementOrDocumentFragment );
+
+		return hostElement || this._domToViewMapping.get( domElementOrDocumentFragment );
 	}
 
 	/**
@@ -625,7 +633,8 @@ export default class DomConverter {
 	 * If this is a first child in the parent and the parent is a {@link module:engine/view/domconverter~DomConverter#bindElements bound}
 	 * element, it is used to find the corresponding text node.
 	 *
-	 * 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.
 	 *
 	 * Otherwise `null` is returned.
 	 *
@@ -640,11 +649,11 @@ export default class DomConverter {
 			return null;
 		}
 
-		// 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;
 		}
 
 		const previousSibling = domText.previousSibling;
@@ -858,13 +867,13 @@ export default class DomConverter {
 	}
 
 	/**
-	 * 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 );
 
 		// Remove domNode from the list.
@@ -874,7 +883,7 @@ export default class DomConverter {
 			const domNode = ancestors.pop();
 			const viewNode = this._domToViewMapping.get( domNode );
 
-			if ( viewNode && viewNode.is( 'uiElement' ) ) {
+			if ( viewNode && ( viewNode.is( 'uiElement' ) || viewNode.is( 'rawElement' ) ) ) {
 				return viewNode;
 			}
 		}
@@ -887,7 +896,8 @@ export default class DomConverter {
 	 *
 	 * 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 the DOM element which represents {@link module:engine/view/uielement~UIElement a view UI element}.
+	 * * inside the DOM element which represents {@link module:engine/view/rawelement~RawElement a view raw element}.
 	 *
 	 * @param {Selection} domSelection DOM Selection object to be checked.
 	 * @returns {Boolean} `true` if the given selection is at a correct place, `false` otherwise.
@@ -919,9 +929,10 @@ export default class DomConverter {
 
 		const viewParent = this.mapDomToView( domParent );
 
-		// 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;
 		}
 

+ 152 - 0
packages/ckeditor5-engine/tests/view/domconverter/rawelement.js

@@ -0,0 +1,152 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document, HTMLElement */
+
+import ViewRawElement from '../../../src/view/rawelement';
+import ViewContainer from '../../../src/view/containerelement';
+import DomConverter from '../../../src/view/domconverter';
+import ViewDocument from '../../../src/view/document';
+import { StylesProcessor } from '../../../src/view/stylesmap';
+
+describe( 'DOMConverter RawElement integration', () => {
+	let converter, viewDocument;
+
+	function createRawElement( name ) {
+		const element = new ViewRawElement( viewDocument, name );
+
+		element.render = function( domDocument ) {
+			const root = this.toDomElement( domDocument );
+			root.innerHTML = '<p><span>foo</span> bar</p>';
+
+			return root;
+		};
+
+		return element;
+	}
+
+	beforeEach( () => {
+		viewDocument = new ViewDocument( new StylesProcessor() );
+		converter = new DomConverter( viewDocument );
+	} );
+
+	describe( 'viewToDom()', () => {
+		it( 'should create a DOM element from a RawElement', () => {
+			const rawElement = new ViewRawElement( viewDocument, 'div' );
+			const domElement = converter.viewToDom( rawElement, document );
+
+			expect( domElement ).to.be.instanceOf( HTMLElement );
+		} );
+
+		it( 'should create a DOM structure from a RawElement', () => {
+			const myElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( myElement, document );
+
+			expect( domElement ).to.be.instanceOf( HTMLElement );
+			expect( domElement.innerHTML ).to.equal( '<p><span>foo</span> bar</p>' );
+		} );
+
+		it( 'should create a DOM structure entirely mapped to a single RawElement', () => {
+			const myElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( myElement, document, { bind: true } );
+			const domParagraph = domElement.childNodes[ 0 ];
+
+			expect( converter.mapDomToView( domElement ) ).to.equal( myElement );
+			expect( converter.mapDomToView( domParagraph ) ).to.equal( myElement );
+			expect( converter.mapDomToView( domParagraph.childNodes[ 0 ] ) ).to.equal( myElement );
+		} );
+	} );
+
+	describe( 'domToView()', () => {
+		it( 'should return a RawElement', () => {
+			const rawElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( rawElement, document, { bind: true } );
+
+			expect( converter.domToView( domElement ) ).to.equal( rawElement );
+		} );
+
+		it( 'should return a RawElement for all nodes inside of it', () => {
+			const rawElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( rawElement, document, { bind: true } );
+
+			const domParagraph = domElement.childNodes[ 0 ];
+			const domSpan = domParagraph.childNodes[ 0 ];
+
+			expect( converter.domToView( domParagraph ) ).to.equal( rawElement );
+			expect( converter.domToView( domSpan ) ).to.equal( rawElement );
+			expect( converter.domToView( domParagraph.childNodes[ 0 ] ) ).equal( rawElement );
+			expect( converter.domToView( domSpan.childNodes[ 0 ] ) ).equal( rawElement );
+		} );
+	} );
+
+	describe( 'domPositionToView()', () => {
+		it( 'should convert a position inside a RawElement to a position before it', () => {
+			const rawElement = createRawElement( 'h1' );
+			const container = new ViewContainer( viewDocument, 'div', null, [ new ViewContainer( viewDocument, 'div' ), rawElement ] );
+			const domContainer = converter.viewToDom( container, document, { bind: true } );
+
+			const viewPosition = converter.domPositionToView( domContainer.childNodes[ 1 ], 0 );
+
+			expect( viewPosition.parent ).to.equal( container );
+			expect( viewPosition.offset ).to.equal( 1 );
+		} );
+
+		it( 'should convert a position inside RawElement children to a position before it', () => {
+			const rawElement = createRawElement( 'h1' );
+			const container = new ViewContainer( viewDocument, 'div', null, [ new ViewContainer( viewDocument, 'div' ), rawElement ] );
+			const domContainer = converter.viewToDom( container, document, { bind: true } );
+
+			const viewPosition = converter.domPositionToView( domContainer.childNodes[ 1 ].childNodes[ 0 ], 1 );
+
+			expect( viewPosition.parent ).to.equal( container );
+			expect( viewPosition.offset ).to.equal( 1 );
+		} );
+	} );
+
+	describe( 'mapDomToView()', () => {
+		it( 'should return a RawElement for all DOM elements inside of it', () => {
+			const myElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( myElement, document, { bind: true } );
+
+			expect( converter.mapDomToView( domElement ) ).to.equal( myElement );
+
+			const domParagraph = domElement.childNodes[ 0 ];
+			expect( converter.mapDomToView( domParagraph ) ).to.equal( myElement );
+
+			const domSpan = domParagraph.childNodes[ 0 ];
+			expect( converter.mapDomToView( domSpan ) ).to.equal( myElement );
+		} );
+	} );
+
+	describe( 'findCorrespondingViewText()', () => {
+		it( 'should return a RawElement for all DOM text nodes inside of it', () => {
+			const myElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( myElement, document, { bind: true } );
+
+			const domText = domElement.querySelector( 'span' ).childNodes[ 0 ];
+			expect( converter.findCorrespondingViewText( domText ) ).to.equal( myElement );
+		} );
+	} );
+
+	describe( 'getHostViewElement()', () => {
+		it( 'should return a RawElement for all DOM children', () => {
+			const rawElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( rawElement, document, { bind: true } );
+
+			const domParagraph = domElement.childNodes[ 0 ];
+			const domSpan = domParagraph.childNodes[ 0 ];
+
+			expect( converter.getHostViewElement( domParagraph ) ).to.equal( rawElement );
+			expect( converter.getHostViewElement( domSpan ) ).to.equal( rawElement );
+		} );
+
+		it( 'should return "null" for the parent itself', () => {
+			const rawElement = createRawElement( 'div' );
+			const domElement = converter.viewToDom( rawElement, document, { bind: true } );
+
+			expect( converter.getHostViewElement( domElement ) ).to.be.null;
+		} );
+	} );
+} );

+ 4 - 4
packages/ckeditor5-engine/tests/view/domconverter/uielement.js

@@ -130,7 +130,7 @@ describe( 'DOMConverter UIElement integration', () => {
 		} );
 	} );
 
-	describe( 'getParentUIElement()', () => {
+	describe( 'getHostViewElement()', () => {
 		it( 'should return UIElement for DOM children', () => {
 			const uiElement = createUIElement( 'div' );
 			const domElement = converter.viewToDom( uiElement, document, { bind: true } );
@@ -138,15 +138,15 @@ describe( 'DOMConverter UIElement integration', () => {
 			const domParagraph = domElement.childNodes[ 0 ];
 			const domSpan = domParagraph.childNodes[ 0 ];
 
-			expect( converter.getParentUIElement( domParagraph ) ).to.equal( uiElement );
-			expect( converter.getParentUIElement( domSpan ) ).to.equal( uiElement );
+			expect( converter.getHostViewElement( domParagraph ) ).to.equal( uiElement );
+			expect( converter.getHostViewElement( domSpan ) ).to.equal( uiElement );
 		} );
 
 		it( 'should return null for element itself', () => {
 			const uiElement = createUIElement( 'div' );
 			const domElement = converter.viewToDom( uiElement, document, { bind: true } );
 
-			expect( converter.getParentUIElement( domElement ) ).to.be.null;
+			expect( converter.getHostViewElement( domElement ) ).to.be.null;
 		} );
 	} );
 } );