Skip to content

Commit

Permalink
Merge pull request #7621 from ckeditor/i/4469-view-raw-element
Browse files Browse the repository at this point in the history
Feature (engine): Implemented the view `RawElement`. Implemented the `DowncastWriter#createRawElement()` method. Closes #4469.

Fix (media-embed): The editor placeholder should disappear after inserting media into an empty editor. Closes #1684.

Docs (ckeditor5): Used the `RawElement` instead of `UIElement` in the "Using a React component in a block widget" guide (see #1684).

Internal (media-embed): Removed the `getFillerOffset()` hack from the `createMediaFigureElement()` helper that is no longer needed since the media content is rendered using `RawElements` (see #1684).

BREAKING CHANGE (engine): The `DomConverter#getParentUIElement()` method was renamed to `DomConverter#getHostViewElement()` because now it supports both `UIElement` and `RawElement` (see #4469).
  • Loading branch information
Reinmar committed Jul 22, 2020
2 parents ea28149 + b224e97 commit bff38e3
Show file tree
Hide file tree
Showing 30 changed files with 1,021 additions and 106 deletions.
8 changes: 2 additions & 6 deletions docs/_snippets/framework/tutorials/using-react-in-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,13 @@ class ProductPreviewEditing extends Plugin {

// The inner <div class="product__react-wrapper"></div> element.
// This element will host a React <ProductPreview /> component.
const reactWrapper = viewWriter.createUIElement( 'div', {
const reactWrapper = viewWriter.createRawElement( 'div', {
class: 'product__react-wrapper'
}, function( domDocument ) {
const domElement = this.toDomElement( domDocument );

}, function( domElement ) {
// This the place where React renders the actual product preview hosted
// by a UIElement in the view. you are using a function (renderer) passed as
// editor.config.products#productRenderer.
renderProduct( id, domElement );

return domElement;
} );

viewWriter.insert( viewWriter.createPositionAt( section, 0 ), reactWrapper );
Expand Down
3 changes: 2 additions & 1 deletion docs/framework/guides/architecture/editing-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,15 @@ editor.data; // The data pipeline (DataController).

### Element types and custom data

The structure of the view resembles the structure in the DOM very closely. The semantics of HTML is defined in its specification. The view structure comes "DTD-free", so in order to provide additional information and better express the semantics of the content, the view structure implements 5 element types ({@link module:engine/view/containerelement~ContainerElement}, {@link module:engine/view/attributeelement~AttributeElement}, {@link module:engine/view/emptyelement~EmptyElement}, {@link module:engine/view/uielement~UIElement}, and {@link module:engine/view/editableelement~EditableElement}) and so called {@link module:engine/view/element~Element#getCustomProperty "custom properties"} (i.e. custom element properties which are not rendered). This additional information provided by editor features is then used by the {@link module:engine/view/renderer~Renderer} and [converters](#conversion).
The structure of the view resembles the structure in the DOM very closely. The semantics of HTML is defined in its specification. The view structure comes "DTD-free", so in order to provide additional information and better express the semantics of the content, the view structure implements 6 element types ({@link module:engine/view/containerelement~ContainerElement}, {@link module:engine/view/attributeelement~AttributeElement}, {@link module:engine/view/emptyelement~EmptyElement}, {@link module:engine/view/rawelement~RawElement}, {@link module:engine/view/uielement~UIElement}, and {@link module:engine/view/editableelement~EditableElement}) and so called {@link module:engine/view/element~Element#getCustomProperty "custom properties"} (i.e. custom element properties which are not rendered). This additional information provided by editor features is then used by the {@link module:engine/view/renderer~Renderer} and [converters](#conversion).

The element types can be defined as follows:

* **Container element** &ndash; The elements that build the structure of the content. Used for block elements such as `<p>`, `<h1>`, `<blockQuote>`, `<li>`, etc.
* **Attribute element** &ndash; The elements that cannot contain container elements inside them. Most model text attributes are converted to view attribute elements. They are used mostly for inline styling elements such as `<strong>`, `<i>`, `<a>`, `<code>`. Similar attribute elements are flattened by the view writer, so e.g. `<a href="..."><a class="bar">x</a></a>` would automatically be optimized to `<a href="..." class="bar">x</a>`.
* **Empty element** &ndash; The elements that must not have any child nodes, for example `<img>`.
* **UI elements** &ndash; The elements that are not a part of the "data" but need to be "inlined" in the content. They are ignored by the selection (it jumps over them) and the view writer in general. The contents of these elements and events coming from them are filtered out, too.
* **Raw element** &ndash; The elements that work as data containers ("wrappers", "sandboxes") but their children are transparent to the editor. Useful when non-standard data must be rendered but the editor should not be concerned what it is and how it works. Users cannot put the selection inside a raw element, split it into smaller chunks or directly modify its content.
* **Editable element** &ndash; The elements used as "nested editables" of non-editable fragments of the content, for example a caption in the image widget, where the `<figure>` wrapping the image is not editable (it is a widget) and the `<figcaption>` inside it is an editable element.

Additionally, you can define {@link module:engine/view/element~Element#getCustomProperty custom properties} which can be used to store information like:
Expand Down
16 changes: 4 additions & 12 deletions docs/framework/guides/tutorials/using-react-in-a-widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,17 +365,13 @@ export default class ProductPreviewEditing extends Plugin {

// The inner <div class="product__react-wrapper"></div> element.
// This element will host a React <ProductPreview /> component.
const reactWrapper = viewWriter.createUIElement( 'div', {
const reactWrapper = viewWriter.createRawElement( 'div', {
class: 'product__react-wrapper'
}, function( domDocument ) {
const domElement = this.toDomElement( domDocument );

}, function( domElement ) {
// This the place where React renders the actual product preview hosted
// by a UIElement in the view. You are using a function (renderer) passed as
// editor.config.products#productRenderer.
renderProduct( id, domElement );

return domElement;
} );

viewWriter.insert( viewWriter.createPositionAt( section, 0 ), reactWrapper );
Expand Down Expand Up @@ -1184,17 +1180,13 @@ export default class ProductPreviewEditing extends Plugin {

// The inner <div class="product__react-wrapper"></div> element.
// This element will host a React <ProductPreview /> component.
const reactWrapper = viewWriter.createUIElement( 'div', {
const reactWrapper = viewWriter.createRawElement( 'div', {
class: 'product__react-wrapper'
}, function( domDocument ) {
const domElement = this.toDomElement( domDocument );

}, function( domElement ) {
// This the place where React renders the actual product preview hosted
// by a UIElement in the view. You are using a function (renderer) passed as
// editor.config.products#productRenderer.
renderProduct( id, domElement );

return domElement;
} );

viewWriter.insert( viewWriter.createPositionAt( section, 0 ), reactWrapper );
Expand Down
32 changes: 25 additions & 7 deletions packages/ckeditor5-engine/src/dev-utils/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import AttributeElement from '../view/attributeelement';
import ContainerElement from '../view/containerelement';
import EmptyElement from '../view/emptyelement';
import UIElement from '../view/uielement';
import RawElement from '../view/rawelement';
import { StylesProcessor } from '../view/stylesmap';

const ELEMENT_RANGE_START_TOKEN = '[';
Expand All @@ -35,7 +36,8 @@ const allowedTypes = {
'container': ContainerElement,
'attribute': AttributeElement,
'empty': EmptyElement,
'ui': UIElement
'ui': UIElement,
'raw': RawElement
};

/**
Expand All @@ -55,6 +57,8 @@ const allowedTypes = {
* (`<span id="marker:foo">`).
* @param {Boolean} [options.renderUIElements=false] When set to `true`, the inner content of each
* {@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.
*/
export function getData( view, options = {} ) {
Expand All @@ -70,6 +74,7 @@ export function getData( view, options = {} ) {
showType: options.showType,
showPriority: options.showPriority,
renderUIElements: options.renderUIElements,
renderRawElements: options.renderRawElements,
ignoreRoot: true
};

Expand Down Expand Up @@ -234,6 +239,8 @@ setData._parse = parse;
* `{` and `}` and the selection outside the text as `[` and `]`. When set to `false`, both will be marked as `[` and `]` only.
* @param {Boolean} [options.renderUIElements=false] When set to `true`, the inner content of each
* {@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.
*/
export function stringify( node, selectionOrPositionOrRange = null, options = {} ) {
Expand Down Expand Up @@ -622,6 +629,8 @@ class ViewStringify {
* `{` and `}` and the selection outside the text as `[` and `]`. When set to `false`, both are marked as `[` and `]`.
* @param {Boolean} [options.renderUIElements=false] When set to `true`, the inner content of each
* {@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.
*/
constructor( root, selection, options ) {
this.root = root;
Expand All @@ -638,6 +647,7 @@ class ViewStringify {
this.ignoreRoot = !!options.ignoreRoot;
this.sameSelectionCharacters = !!options.sameSelectionCharacters;
this.renderUIElements = !!options.renderUIElements;
this.renderRawElements = !!options.renderRawElements;
}

/**
Expand Down Expand Up @@ -670,8 +680,15 @@ class ViewStringify {
callback( this._stringifyElementOpen( root ) );
}

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 {
let offset = 0;
callback( this._stringifyElementRanges( root, offset ) );
Expand Down Expand Up @@ -824,8 +841,9 @@ class ViewStringify {
* Returns:
* * 'attribute' for {@link module:engine/view/attributeelement~AttributeElement attribute elements},
* * '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.
*
* @private
Expand Down Expand Up @@ -943,10 +961,10 @@ function _convertViewElements( rootNode ) {
for ( const child of [ ...rootNode.getChildren() ] ) {
if ( convertedElement.is( 'emptyElement' ) ) {
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.' );
}

convertedElement._appendChild( _convertViewElements( child ) );
Expand Down
70 changes: 42 additions & 28 deletions packages/ckeditor5-engine/src/view/domconverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -236,6 +236,12 @@ export default class DomConverter {
domElement = domDocument.createElement( viewNode.name );
}

// 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 ) {
this.bindElements( domElement, viewNode );
}
Expand Down Expand Up @@ -392,11 +398,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 ) ) {
Expand Down Expand Up @@ -550,10 +556,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 );
}

Expand Down Expand Up @@ -605,14 +611,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 );
}

/**
Expand All @@ -625,7 +635,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.
*
Expand All @@ -640,11 +651,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;
Expand Down Expand Up @@ -858,13 +869,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.
Expand All @@ -874,7 +885,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;
}
}
Expand All @@ -886,8 +897,10 @@ export default class DomConverter {
* Checks if given selection's boundaries are at correct places.
*
* 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}.
*
* @param {Selection} domSelection DOM Selection object to be checked.
* @returns {Boolean} `true` if the given selection is at a correct place, `false` otherwise.
Expand Down Expand Up @@ -919,9 +932,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;
}

Expand Down
Loading

0 comments on commit bff38e3

Please sign in to comment.