Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#4469: Implemented the view RawElement #7621

Merged
merged 31 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f6d139d
Created a RawElement class.
oleq Jul 13, 2020
2deb00b
Added support for RawElement in getData(), stringify(), and parse() v…
oleq Jul 13, 2020
e694001
Added RawElement support in the MutationObserver.
oleq Jul 13, 2020
eec1c98
Made it possible to use the RawElement in the DomConverter.
oleq Jul 13, 2020
c1e0eba
Integrated the Renderer class with RawElement.
oleq Jul 13, 2020
3ddcc77
Integrated the DowncastWriter with the RawElement.
oleq Jul 13, 2020
1932a7a
Made the Media Embed feature use RawElement instead of UIElement.
oleq Jul 13, 2020
50fbc80
Tests: Added a test that verifies the placeholder feature no longer c…
oleq Jul 13, 2020
b56be5d
Docs: Used the RawElement in the "Using a React component in a block …
oleq Jul 13, 2020
c467a3b
Docs: Mentioned RawElement in the "Editing engine" guide.
oleq Jul 13, 2020
a5208bf
Tests: Added a comment in media embed integration test.
oleq Jul 13, 2020
8271513
Docs: Minor docs correction.
oleq Jul 13, 2020
b3f21a1
Tests: Added tests for RawElement class.
oleq Jul 14, 2020
bbe1ffc
Tests: Code refactoring.
oleq Jul 14, 2020
5aaffb6
Merge branch 'master' into i/4469-view-raw-element
oleq Jul 17, 2020
1fa577f
Simplified RawElement#render(). Added docs to the class.
oleq Jul 17, 2020
5e9cefb
Tests: Updated RawElement tests after #render() simplification.
oleq Jul 17, 2020
bc1a607
Simplified the DowncastWriter#createRawElement() method.
oleq Jul 17, 2020
d7483a1
Made sure editing view creates the RawElement. Updated docs.
oleq Jul 17, 2020
60476ef
Tests: Updated view stringify test helper after changes to the RawEle…
oleq Jul 17, 2020
777a1b3
Tests: Updated view renderer tests after changes to RawElement render…
oleq Jul 17, 2020
f298535
Tests: Updated mutation observer tests after changes to RawElement re…
oleq Jul 17, 2020
1d771af
Used the new DowncastWriter#createRawElement API in the MediaRegistry.
oleq Jul 17, 2020
ff1d7e2
Docs: Used the new DowncastWriter#createRawElement API in the "Using …
oleq Jul 17, 2020
9969d1e
Docs: Improved the description of raw elements in the "Editing engine…
oleq Jul 17, 2020
ea42b0d
toWidget() should throw when attempting to create a widget out of a n…
oleq Jul 21, 2020
4885381
Tests: Added a DowncastWriter#createRawElement() test to check if Raw…
oleq Jul 21, 2020
2b91300
Merge branch 'master' into i/4469-view-raw-element
oleq Jul 22, 2020
a13b288
Docs: Updated DowncastWriter#createUIElement() and #createRawElement(…
oleq Jul 22, 2020
9b23bf7
Merge branch 'master' into i/4469-view-raw-element
Reinmar Jul 22, 2020
b224e97
Removed confusing examples.
Reinmar Jul 22, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
AnnaTomanek marked this conversation as resolved.
Show resolved Hide resolved
* **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