8
0
Просмотр исходного кода

Merge pull request #1579 from ckeditor/t/1549

Feature: Introduced `createDocumentFragment()`, `createElement()` and `createText()` methods in `UpcastWriter`. Additionally, the `View.change()` method now returns the return value of its callback. Closes #1549.
Piotrek Koszuliński 7 лет назад
Родитель
Сommit
edd457b75a

+ 4 - 3
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -38,7 +38,7 @@ import { isPlainObject } from 'lodash-es';
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 
 /**
- * Writes the content of the {@link module:engine/model/document~Document document} to an HTML-like string.
+ * Writes the content of a model {@link module:engine/model/document~Document document} to an HTML-like string.
  *
  * **Note:** A {@link module:engine/model/text~Text text} node that contains attributes will be represented as:
  *
@@ -72,9 +72,10 @@ export function getData( model, options = {} ) {
 getData._stringify = stringify;
 
 /**
- * Sets the content of the {@link module:engine/model/document~Document document} provided as an HTML-like string.
+ * Sets the content of a model {@link module:engine/model/document~Document document} provided as an HTML-like string.
  *
- * **Note:** Remember to register elements in the {@link module:engine/model/model~Model#schema model's schema} before inserting them.
+ * **Note:** Remember to register elements in the {@link module:engine/model/model~Model#schema model's schema} before
+ * trying to use them.
  *
  * **Note:** To create a {@link module:engine/model/text~Text text} node that contains attributes use:
  *

+ 34 - 31
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -80,7 +80,7 @@ export function getData( view, options = {} ) {
 getData._stringify = stringify;
 
 /**
- * Sets the content of the {@link module:engine/view/document~Document document} provided as an HTML-like string.
+ * Sets the content of a view {@link module:engine/view/document~Document document} provided as an HTML-like string.
  *
  * @param {module:engine/view/view~View} view
  * @param {String} data An HTML-like string to write into the document.
@@ -111,44 +111,47 @@ setData._parse = parse;
 
 /**
  * Converts view elements to HTML-like string representation.
+ *
  * A root element can be provided as {@link module:engine/view/text~Text text}:
  *
- *		const text = new Text( 'foobar' );
+ *		const text = downcastWriter.createText( 'foobar' );
  *		stringify( text ); // 'foobar'
  *
  * or as an {@link module:engine/view/element~Element element}:
  *
- *		const element = new Element( 'p', null, new Text( 'foobar' ) );
+ *		const element = downcastWriter.createElement( 'p', null, downcastWriter.createText( 'foobar' ) );
  *		stringify( element ); // '<p>foobar</p>'
  *
  * or as a {@link module:engine/view/documentfragment~DocumentFragment document fragment}:
  *
- *		const text = new Text( 'foobar' );
- *		const b = new Element( 'b', { name: 'test' }, text );
- *		const p = new Element( 'p', { style: 'color:red;' } );
- *		const fragment = new DocumentFragment( [ p, b ] );
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const b = downcastWriter.createElement( 'b', { name: 'test' }, text );
+ *		const p = downcastWriter.createElement( 'p', { style: 'color:red;' } );
+ *		const fragment = downcastWriter.createDocumentFragment( [ p, b ] );
  *
  *		stringify( fragment ); // '<p style="color:red;"></p><b name="test">foobar</b>'
  *
  * Additionally, a {@link module:engine/view/documentselection~DocumentSelection selection} instance can be provided.
- * Ranges from the selection will then be included in output data.
+ * Ranges from the selection will then be included in the output data.
  * If a range position is placed inside the element node, it will be represented with `[` and `]`:
  *
- *		const text = new Text( 'foobar' );
- *		const b = new Element( 'b', null, text );
- *		const p = new Element( 'p', null, b );
- *		const selection = new Selection(
- *			Range._createFromParentsAndOffsets( p, 0, p, 1 )
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const b = downcastWriter.createElement( 'b', null, text );
+ *		const p = downcastWriter.createElement( 'p', null, b );
+ *		const selection = downcastWriter.createSelection(
+ *			downcastWriter.createRangeIn( p )
  *		);
  *
  *		stringify( p, selection ); // '<p>[<b>foobar</b>]</p>'
  *
  * If a range is placed inside the text node, it will be represented with `{` and `}`:
  *
- *		const text = new Text( 'foobar' );
- *		const b = new Element( 'b', null, text );
- *		const p = new Element( 'p', null, b );
- *		const selection = new Selection( Range._createFromParentsAndOffsets( text, 1, text, 5 ) );
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const b = downcastWriter.createElement( 'b', null, text );
+ *		const p = downcastWriter.createElement( 'p', null, b );
+ *		const selection = downcastWriter.createSelection(
+ *			downcastWriter.createRange( downcastWriter.createPositionAt( text, 1 ), downcastWriter.createPositionAt( text, 5 ) )
+ *		);
  *
  *		stringify( p, selection ); // '<p><b>f{ooba}r</b></p>'
  *
@@ -159,10 +162,10 @@ setData._parse = parse;
  *
  * Multiple ranges are supported:
  *
- *		const text = new Text( 'foobar' );
- *		const selection = new Selection( [
- *			Range._createFromParentsAndOffsets( text, 0, text, 1 ) ),
- *			Range._createFromParentsAndOffsets( text, 3, text, 5 ) )
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const selection = downcastWriter.createSelection( [
+ *			downcastWriter.createRange( downcastWriter.createPositionAt( text, 0 ), downcastWriter.createPositionAt( text, 1 ) ),
+ *			downcastWriter.createRange( downcastWriter.createPositionAt( text, 3 ), downcastWriter.createPositionAt( text, 5 ) )
  *		] );
  *
  *		stringify( text, selection ); // '{f}oo{ba}r'
@@ -172,9 +175,9 @@ setData._parse = parse;
  * is provided, it will be converted to a selection containing this range. If a position instance is provided, it will
  * be converted to a selection containing one range collapsed at this position.
  *
- *		const text = new Text( 'foobar' );
- *		const range = Range._createFromParentsAndOffsets( text, 0, text, 1 );
- *		const position = new Position( text, 3 );
+ *		const text = downcastWriter.createText( 'foobar' );
+ *		const range = downcastWriter.createRange( downcastWriter.createPositionAt( text, 0 ), downcastWriter.createPositionAt( text, 1 ) );
+ *		const position = downcastWriter.createPositionAt( text, 3 );
  *
  *		stringify( text, range ); // '{f}oobar'
  *		stringify( text, position ); // 'foo{}bar'
@@ -186,10 +189,10 @@ setData._parse = parse;
  * {@link module:engine/view/emptyelement~EmptyElement empty elements}
  * and {@link module:engine/view/uielement~UIElement UI elements}:
  *
- *		const attribute = new AttributeElement( 'b' );
- *		const container = new ContainerElement( 'p' );
- *		const empty = new EmptyElement( 'img' );
- *		const ui = new UIElement( 'span' );
+ *		const attribute = downcastWriter.createAttributeElement( 'b' );
+ *		const container = downcastWriter.createContainerElement( 'p' );
+ *		const empty = downcastWriter.createEmptyElement( 'img' );
+ *		const ui = downcastWriter.createUIElement( 'span' );
  *		getData( attribute, null, { showType: true } ); // '<attribute:b></attribute:b>'
  *		getData( container, null, { showType: true } ); // '<container:p></container:p>'
  *		getData( empty, null, { showType: true } ); // '<empty:img></empty:img>'
@@ -198,14 +201,14 @@ setData._parse = parse;
  * If `options.showPriority` is set to `true`, a priority will be displayed for all
  * {@link module:engine/view/attributeelement~AttributeElement attribute elements}.
  *
- *		const attribute = new AttributeElement( 'b' );
+ *		const attribute = downcastWriter.createAttributeElement( 'b' );
  *		attribute._priority = 20;
  *		getData( attribute, null, { showPriority: true } ); // <b view-priority="20"></b>
  *
  * If `options.showAttributeElementId` is set to `true`, the attribute element's id will be displayed for all
  * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that have it set.
  *
- *		const attribute = new AttributeElement( 'span' );
+ *		const attribute = downcastWriter.createAttributeElement( 'span' );
  *		attribute._id = 'marker:foo';
  *		getData( attribute, null, { showAttributeElementId: true } ); // <span view-id="marker:foo"></span>
  *
@@ -249,7 +252,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
 }
 
 /**
- * Parses an HTML-like string and returns view tree nodes.
+ * Parses an HTML-like string and returns a view tree.
  * A simple string will be converted to a {@link module:engine/view/text~Text text} node:
  *
  *		parse( 'foobar' ); // Returns an instance of text.

+ 10 - 7
packages/ckeditor5-engine/src/view/attributeelement.js

@@ -14,22 +14,25 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 const DEFAULT_PRIORITY = 10;
 
 /**
- * Attributes are elements which define document presentation. They are mostly elements like `<b>` or `<span>`.
- * Attributes can be broken and merged by the {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer}.
+ * Attribute elements are used to represent formatting elements in the view (think – `<b>`, `<span style="font-size: 2em">`, etc.).
+ * Most often they are created when downcasting model text attributes.
  *
- * Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
- * be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement}
- * class or `AttributeElement`.
+ * Editing engine does not define a fixed HTML DTD. This is why a feature developer needs to choose between various
+ * types (container element, {@link module:engine/view/attributeelement~AttributeElement attribute element},
+ * {@link module:engine/view/emptyelement~EmptyElement empty element}, etc) when developing a feature.
+ *
+ * To create a new attribute element instance use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `DowncastWriter#createAttributeElement()`} method.
  *
  * @extends module:engine/view/element~Element
  */
 export default class AttributeElement extends Element {
 	/**
-	 * Creates a attribute element.
+	 * Creates an attribute element.
 	 *
 	 * @see module:engine/view/downcastwriter~DowncastWriter#createAttributeElement
-	 * @protected
 	 * @see module:engine/view/element~Element
+	 * @protected
 	 */
 	constructor( name, attrs, children ) {
 		super( name, attrs, children );

+ 13 - 26
packages/ckeditor5-engine/src/view/containerelement.js

@@ -11,35 +11,22 @@ import Element from './element';
 
 /**
  * Containers are elements which define document structure. They define boundaries for
- * {@link module:engine/view/attributeelement~AttributeElement attributes}. They are mostly use for block elements like `<p>` or `<div>`.
+ * {@link module:engine/view/attributeelement~AttributeElement attributes}. They are mostly used for block elements like `<p>` or `<div>`.
  *
- * Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to
- * be defined by the feature developer.
+ * Editing engine does not define a fixed HTML DTD. This is why a feature developer needs to choose between various
+ * types (container element, {@link module:engine/view/attributeelement~AttributeElement attribute element},
+ * {@link module:engine/view/emptyelement~EmptyElement empty element}, etc) when developing a feature.
  *
- * Creating an element you should use `ContainerElement` class or {@link module:engine/view/attributeelement~AttributeElement}. This is
- * important to define the type of the element because of two reasons:
+ * The container element should be your default choice when writing a converter, unless:
  *
- * Firstly, {@link module:engine/view/domconverter~DomConverter} needs the information what is an editable block to convert elements to
- * DOM properly. {@link module:engine/view/domconverter~DomConverter} will ensure that `ContainerElement` is editable and it is possible
- * to put caret inside it, even if the container is empty.
+ * * this element represents a model text attribute (then use {@link module:engine/view/attributeelement~AttributeElement}),
+ * * this is an empty element like `<img>` (then use {@link module:engine/view/emptyelement~EmptyElement}),
+ * * this is a root element,
+ * * this is a nested editable element (then use  {@link module:engine/view/editableelement~EditableElement}).
  *
- * Secondly, {@link module:engine/view/downcastwriter~DowncastWriter view downcast writer} uses this information.
- * Nodes {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breaking} and
- * {@link module:engine/view/downcastwriter~DowncastWriter#mergeAttributes merging} is performed only in a bounds of a container nodes.
- *
- * For instance if `<p>` is an container and `<b>` is attribute:
- *
- *		<p><b>fo^o</b></p>
- *
- * {@link module:engine/view/downcastwriter~DowncastWriter#breakAttributes breakAttributes} will create:
- *
- *		<p><b>fo</b><b>o</b></p>
- *
- * There might be a need to mark `<span>` element as a container node, for example in situation when it will be a
- * container of an inline widget:
- *
- *		<span color="red">foobar</span>		// attribute
- *		<span data-widget>foobar</span>		// container
+ * To create a new container element instance use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `DowncastWriter#createContainerElement()`}
+ * method.
  *
  * @extends module:engine/view/element~Element
  */
@@ -47,8 +34,8 @@ export default class ContainerElement extends Element {
 	/**
 	 * Creates a container element.
 	 *
-	 * @see module:engine/view/element~Element
 	 * @see module:engine/view/downcastwriter~DowncastWriter#createContainerElement
+	 * @see module:engine/view/element~Element
 	 * @protected
 	 */
 	constructor( name, attrs, children ) {

+ 7 - 3
packages/ckeditor5-engine/src/view/documentfragment.js

@@ -14,15 +14,19 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 
 /**
- * DocumentFragment class.
+ * Document fragment.
+ *
+ * To create a new document fragment instance use the
+ * {@link module:engine/view/upcastwriter~UpcastWriter#createDocumentFragment `UpcastWriter#createDocumentFragment()`}
+ * method.
  */
 export default class DocumentFragment {
 	/**
 	 * Creates new DocumentFragment instance.
 	 *
 	 * @protected
-	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children] List of nodes to be inserted into
-	 * created document fragment.
+	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
+	 * A list of nodes to be inserted into the created document fragment.
 	 */
 	constructor( children ) {
 		/**

+ 5 - 4
packages/ckeditor5-engine/src/view/downcastwriter.js

@@ -27,10 +27,11 @@ import { isPlainObject } from 'lodash-es';
  * It provides a set of methods used to manipulate view nodes.
  *
  * The `DowncastWriter` is designed to work with semantic views which are the views that were/are being downcasted from the model.
- * To work with ordinary views (e.g. parsed from a string) use the {@link module:engine/view/upcastwriter~UpcastWriter upcast writer}.
+ * To work with ordinary views (e.g. parsed from a pasted content) use the
+ * {@link module:engine/view/upcastwriter~UpcastWriter upcast writer}.
  *
  * Do not create an instance of this writer manually. To modify a view structure, use
- * the {@link module:engine/view/view~View#change View#change()) block.
+ * the {@link module:engine/view/view~View#change `View#change()`) block.
  */
 export default class DowncastWriter {
 	constructor( document ) {
@@ -141,8 +142,8 @@ export default class DowncastWriter {
 	 *
 	 *		writer.createText( 'foo' );
 	 *
-	 * @param {String} data Text data.
-	 * @returns {module:engine/view/text~Text} Created text node.
+	 * @param {String} data The text's data.
+	 * @returns {module:engine/view/text~Text} The created text node.
 	 */
 	createText( data ) {
 		return new Text( data );

+ 3 - 0
packages/ckeditor5-engine/src/view/editableelement.js

@@ -20,6 +20,9 @@ const documentSymbol = Symbol( 'document' );
  *
  * Editable is automatically read-only when its {@link module:engine/view/document~Document Document} is read-only.
  *
+ * The constructor of this class shouldn't be used directly. To create new `EditableElement` use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `downcastWriter#createEditableElement()`} method.
+ *
  * @extends module:engine/view/containerelement~ContainerElement
  * @mixes module:utils/observablemixin~ObservableMixin
  */

+ 15 - 21
packages/ckeditor5-engine/src/view/element.js

@@ -22,20 +22,21 @@ import { isPlainObject } from 'lodash-es';
  * This is why the type of the {@link module:engine/view/element~Element} need to
  * be defined by the feature developer. When creating an element you should use one of the following methods:
  *
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `writer.createContainerElement()`} in order to create
- * a {@link module:engine/view/containerelement~ContainerElement},
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `writer.createAttributeElement()`} in order to create
- * a {@link module:engine/view/attributeelement~AttributeElement},
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `writer.createEmptyElement()`} in order to create
- * a {@link module:engine/view/emptyelement~EmptyElement}.
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `writer.createUIElement()`} in order to create
- * a {@link module:engine/view/uielement~UIElement}.
- * * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `writer.createEditableElement()`} in order to create
- * a {@link module:engine/view/editableelement~EditableElement}.
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement `downcastWriter#createContainerElement()`}
+ * in order to create a {@link module:engine/view/containerelement~ContainerElement},
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement `downcastWriter#createAttributeElement()`}
+ * in order to create a {@link module:engine/view/attributeelement~AttributeElement},
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`}
+ * in order to create a {@link module:engine/view/emptyelement~EmptyElement}.
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`}
+ * in order to create a {@link module:engine/view/uielement~UIElement}.
+ * * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement `downcastWriter#createEditableElement()`}
+ * in order to create a {@link module:engine/view/editableelement~EditableElement}.
  *
  * Note that for view elements which are not created from the model, like elements from mutations, paste or
- * {@link module:engine/controller/datacontroller~DataController#set data.set} it is not possible to define the type of the element, so
- * these will be instances of the {@link module:engine/view/element~Element}.
+ * {@link module:engine/controller/datacontroller~DataController#set data.set} it is not possible to define the type of the element.
+ * In such cases the {@link module:engine/view/upcastwriter~UpcastWriter#createElement `UpcastWriter#createElement()`} method
+ * should be used to create generic view elements.
  *
  * @extends module:engine/view/node~Node
  */
@@ -45,22 +46,15 @@ export default class Element extends Node {
 	 *
 	 * Attributes can be passed in various formats:
 	 *
-	 *		new Element( 'div', { 'class': 'editor', 'contentEditable': 'true' } ); // object
+	 *		new Element( 'div', { class: 'editor', contentEditable: 'true' } ); // object
 	 *		new Element( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
 	 *		new Element( 'div', mapOfAttributes ); // map
 	 *
-	 * **Note:** Constructor of this class shouldn't be used directly in the code. Use the
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createAttributeElement} for inline element,
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createContainerElement} for block element,
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createEditableElement} for editable element,
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement} for empty element or
-	 * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement} for UI element instead.
-	 *
 	 * @protected
 	 * @param {String} name Node name.
 	 * @param {Object|Iterable} [attrs] Collection of attributes.
 	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
-	 * List of nodes to be inserted into created element.
+	 * A list of nodes to be inserted into created element.
 	 */
 	constructor( name, attrs, children ) {
 		super();

+ 4 - 1
packages/ckeditor5-engine/src/view/emptyelement.js

@@ -12,7 +12,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import Node from './node';
 
 /**
- * EmptyElement class. It is used to represent elements that cannot contain any child nodes.
+ * Empty element class. It is used to represent elements that cannot contain any child nodes (for example `<img>` elements).
+ *
+ * To create a new empty element use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createEmptyElement `downcastWriter#createEmptyElement()`} method.
  */
 export default class EmptyElement extends Element {
 	/**

+ 9 - 7
packages/ckeditor5-engine/src/view/node.js

@@ -16,13 +16,15 @@ import { clone } from 'lodash-es';
 /**
  * Abstract tree view node class.
  *
+ * This is an abstract class. Its constructor should not be used directly.
+ * Use the {@link module:engine/view/element~Element} class to create view elements
+ * or {@link module:engine/view/text~Text} class to create view text nodes.
+ *
  * @abstract
  */
 export default class Node {
 	/**
 	 * Creates a tree view node.
-	 *
-	 * This is an abstract class, so this constructor should not be used directly.
 	 */
 	constructor() {
 		/**
@@ -123,11 +125,11 @@ export default class Node {
 	 * Gets a path to the node. The path is an array containing indices of consecutive ancestors of this node,
 	 * beginning from {@link module:engine/view/node~Node#root root}, down to this node's index.
 	 *
-	 *		const abc = new Text( 'abc' );
-	 *		const foo = new Text( 'foo' );
-	 *		const h1 = new Element( 'h1', null, new Text( 'header' ) );
-	 *		const p = new Element( 'p', null, [ abc, foo ] );
-	 *		const div = new Element( 'div', null, [ h1, p ] );
+	 *		const abc = downcastWriter.createText( 'abc' );
+	 *		const foo = downcastWriter.createText( 'foo' );
+	 *		const h1 = downcastWriter.createElement( 'h1', null, downcastWriter.createText( 'header' ) );
+	 *		const p = downcastWriter.createElement( 'p', null, [ abc, foo ] );
+	 *		const div = downcastWriter.createElement( 'div', null, [ h1, p ] );
 	 *		foo.getPath(); // Returns [ 1, 3 ]. `foo` is in `p` which is in `div`. `p` starts at offset 1, while `foo` at 3.
 	 *		h1.getPath(); // Returns [ 0 ].
 	 *		div.getPath(); // Returns [].

+ 7 - 1
packages/ckeditor5-engine/src/view/position.js

@@ -14,7 +14,13 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import EditableElement from './editableelement';
 
 /**
- * Position in the tree. Position is always located before or after a node.
+ * Position in the view tree. Position is represented by its parent node and an offset in this parent.
+ *
+ * In order to create a new position instance use the `createPosition*()` factory methods available in:
+ *
+ * * {@module:engine/view/view~View}
+ * * {@module:engine/view/downcastwriter~DowncastWriter}
+ * * {@module:engine/view/upcastwriter~UpcastWriter}
  */
 export default class Position {
 	/**

+ 36 - 22
packages/ckeditor5-engine/src/view/range.js

@@ -11,7 +11,13 @@ import Position from './position';
 import TreeWalker from './treewalker';
 
 /**
- * Tree view range.
+ * Range in the view tree. A range is represented by its start and end {@link module:engine/view/position~Position positions}.
+ *
+ * In order to create a new position instance use the `createPosition*()` factory methods available in:
+ *
+ * * {@module:engine/view/view~View}
+ * * {@module:engine/view/downcastwriter~DowncastWriter}
+ * * {@module:engine/view/upcastwriter~UpcastWriter}
  */
 export default class Range {
 	/**
@@ -20,7 +26,7 @@ export default class Range {
 	 * **Note:** Constructor creates it's own {@link module:engine/view/position~Position} instances basing on passed values.
 	 *
 	 * @param {module:engine/view/position~Position} start Start position.
-	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
+	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at the `start` position.
 	 */
 	constructor( start, end = null ) {
 		/**
@@ -91,13 +97,14 @@ export default class Range {
 	 *
 	 * For example:
 	 *
-	 * 		<p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p>
-	 * 		<p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
+	 *		<p>Foo</p><p><b>{Bar}</b></p> -> <p>Foo</p>[<p><b>Bar</b>]</p>
+	 *		<p><b>foo</b>{bar}<span></span></p> -> <p><b>foo[</b>bar<span></span>]</p>
 	 *
 	 * Note that in the sample above:
-	 *  - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
-	 *  - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
-	 *  - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
+	 *
+	 * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
+	 * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
+	 * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
 	 *
 	 * @returns {module:engine/view/range~Range} Enlarged range.
 	 */
@@ -123,13 +130,14 @@ export default class Range {
 	 *
 	 * For example:
 	 *
-	 * 		<p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p>
-	 * 		<p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
+	 *		<p>Foo</p>[<p><b>Bar</b>]</p> -> <p>Foo</p><p><b>{Bar}</b></p>
+	 *		<p><b>foo[</b>bar<span></span>]</p> -> <p><b>foo</b>{bar}<span></span></p>
 	 *
 	 * Note that in the sample above:
-	 *  - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
-	 *  - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
-	 *  - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
+	 *
+	 * - `<p>` have type of {@link module:engine/view/containerelement~ContainerElement},
+	 * - `<b>` have type of {@link module:engine/view/attributeelement~AttributeElement},
+	 * - `<span>` have type of {@link module:engine/view/uielement~UIElement}.
 	 *
 	 * @returns {module:engine/view/range~Range} Shrink range.
 	 */
@@ -205,10 +213,10 @@ export default class Range {
 	 *
 	 * Examples:
 	 *
-	 *		let foo = new Text( 'foo' );
-	 *		let img = new ContainerElement( 'img' );
-	 *		let bar = new Text( 'bar' );
-	 *		let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
+	 *		let foo = downcastWriter.createText( 'foo' );
+	 *		let img = downcastWriter.createContainerElement( 'img' );
+	 *		let bar = downcastWriter.createText( 'bar' );
+	 *		let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
 	 *
 	 *		let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
 	 *		let otherRange = view.createRange( // "oo", img, "ba" are in range.
@@ -260,10 +268,10 @@ export default class Range {
 	 *
 	 * Examples:
 	 *
-	 *		let foo = new Text( 'foo' );
-	 *		let img = new ContainerElement( 'img' );
-	 *		let bar = new Text( 'bar' );
-	 *		let p = new ContainerElement( 'p', null, [ foo, img, bar ] );
+	 *		let foo = downcastWriter.createText( 'foo' );
+	 *		let img = downcastWriter.createContainerElement( 'img' );
+	 *		let bar = downcastWriter.createText( 'bar' );
+	 *		let p = downcastWriter.createContainerElement( 'p', null, [ foo, img, bar ] );
 	 *
 	 *		let range = view.createRange( view.createPositionAt( foo, 2 ), view.createPositionAt( bar, 1 ); // "o", img, "b" are in range.
 	 *		let otherRange = view.createRange( view.createPositionAt( foo, 1 ), view.createPositionAt( p, 2 ); // "oo", img are in range.
@@ -309,6 +317,7 @@ export default class Range {
 	 * @param {Boolean} [options.singleCharacters=false]
 	 * @param {Boolean} [options.shallow=false]
 	 * @param {Boolean} [options.ignoreElementEnd=false]
+	 * @returns {module:engine/view/treewalker~TreeWalker}
 	 */
 	getWalker( options = {} ) {
 		options.boundaries = this;
@@ -326,6 +335,11 @@ export default class Range {
 		return this.start.getCommonAncestor( this.end );
 	}
 
+	/**
+	 * Clones this range.
+	 *
+	 * @returns {module:engine/view/range~Range}
+	 */
 	clone() {
 		return new Range( this.start, this.end );
 	}
@@ -381,7 +395,7 @@ export default class Range {
 	}
 
 	/**
-	 * Checks and returns whether this range intersects with given range.
+	 * Checks and returns whether this range intersects with the given range.
 	 *
 	 * @param {module:engine/view/range~Range} otherRange Range to compare with.
 	 * @returns {Boolean} True if ranges intersect.
@@ -391,7 +405,7 @@ export default class Range {
 	}
 
 	/**
-	 * Creates a range from given parents and offsets.
+	 * Creates a range from the given parents and offsets.
 	 *
 	 * @protected
 	 * @param {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment} startElement Start position

+ 9 - 6
packages/ckeditor5-engine/src/view/text.js

@@ -12,17 +12,20 @@ import Node from './node';
 /**
  * Tree view text node.
  *
+ * The constructor of this class shouldn't be used directly. To create new Text instances
+ * use the {@link module:engine/view/downcastwriter~DowncastWriter#createText `DowncastWriter#createText()`}
+ * method when working on data downcasted from the model or the
+ * {@link module:engine/view/upcastwriter~UpcastWriter#createText `UpcastWriter#createText()`}
+ * method when working on non-semantic views.
+ *
  * @extends module:engine/view/node~Node
  */
 export default class Text extends Node {
 	/**
 	 * Creates a tree view text node.
 	 *
-	 * **Note:** Constructor of this class shouldn't be used directly in the code.
-	 * Use the {@link module:engine/view/downcastwriter~DowncastWriter#createText} method instead.
-	 *
 	 * @protected
-	 * @param {String} data Text.
+	 * @param {String} data The text's data.
 	 */
 	constructor( data ) {
 		super();
@@ -57,8 +60,8 @@ export default class Text extends Node {
 	/**
 	 * This getter is required when using the addition assignment operator on protected property:
 	 *
-	 *		const foo = new Text( 'foo' );
-	 *		const bar = new Text( 'bar' );
+	 *		const foo = downcastWriter.createText( 'foo' );
+	 *		const bar = downcastWriter.createText( 'bar' );
 	 *
 	 *		foo._data += bar.data;   // executes: `foo._data = foo._data + bar.data`
 	 *		console.log( foo.data ); // prints: 'foobar'

+ 17 - 3
packages/ckeditor5-engine/src/view/uielement.js

@@ -13,8 +13,22 @@ import Node from './node';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
 
 /**
- * UIElement class. It is used to represent UI not a content of the document.
- * This element can't be split and selection can't be placed inside this element.
+ * UI element class. It should be used to represent editing UI which needs to be injected into the editing view
+ * If possible, you should keep your UI outside the editing view. However, if that is not possible,
+ * UI elements can be used.
+ *
+ * How a UI element is rendered is in your control (you pass a callback to
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`}).
+ * The editor will ignore your UI element – the selection cannot be placed in it, it is skipped (invisible) when
+ * the user modifies the selection by using arrow keys and the editor does not listen to any mutations which
+ * happen inside your UI elements.
+ *
+ * The limitation is that you cannot convert a model element to a UI element. UI elements need to be
+ * created for {@link module:engine/model/markercollection~Marker markers} or as additinal elements
+ * inside normal {@link module:engine/view/containerelement~ContainerElement container elements}.
+ *
+ * To create a new UI element use the
+ * {@link module:engine/view/downcastwriter~DowncastWriter#createUIElement `downcastWriter#createUIElement()`} method.
  */
 export default class UIElement extends Element {
 	/**
@@ -72,7 +86,7 @@ export default class UIElement extends Element {
 	 * {@link module:engine/view/domconverter~DomConverter}.
 	 * Do not use inheritance to create custom rendering method, replace `render()` method instead:
 	 *
-	 *		const myUIElement = new UIElement( 'span' );
+	 *		const myUIElement = downcastWriter.createUIElement( 'span' );
 	 *		myUIElement.render = function( domDocument ) {
 	 *			const domElement = this.toDomElement( domDocument );
 	 *			domElement.innerHTML = '<b>this is ui element</b>';

+ 42 - 0
packages/ckeditor5-engine/src/view/upcastwriter.js

@@ -7,7 +7,9 @@
  * @module module:engine/view/upcastwriter
  */
 
+import DocumentFragment from './documentfragment';
 import Element from './element';
+import Text from './text';
 import { isPlainObject } from 'lodash-es';
 import Position from './position';
 import Range from './range';
@@ -23,6 +25,46 @@ import Selection from './selection';
  * {@link module:engine/view/downcastwriter~DowncastWriter downcast writer}.
  */
 export default class UpcastWriter {
+	/**
+	 * Creates a new {@link module:engine/view/documentfragment~DocumentFragment} instance.
+	 *
+	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
+	 * A list of nodes to be inserted into the created document fragment.
+	 * @returns {module:engine/view/documentfragment~DocumentFragment} The created document fragment.
+	 */
+	createDocumentFragment( children ) {
+		return new DocumentFragment( children );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/view/element~Element} instance.
+	 *
+	 * Attributes can be passed in various formats:
+	 *
+	 *		upcastWriter.createElement( 'div', { class: 'editor', contentEditable: 'true' } ); // object
+	 *		upcastWriter.createElement( 'div', [ [ 'class', 'editor' ], [ 'contentEditable', 'true' ] ] ); // map-like iterator
+	 *		upcastWriter.createElement( 'div', mapOfAttributes ); // map
+	 *
+	 * @param {String} name Node name.
+	 * @param {Object|Iterable} [attrs] Collection of attributes.
+	 * @param {module:engine/view/node~Node|Iterable.<module:engine/view/node~Node>} [children]
+	 * A list of nodes to be inserted into created element.
+	 * @returns {module:engine/view/element~Element} Created element.
+	 */
+	createElement( name, attrs, children ) {
+		return new Element( name, attrs, children );
+	}
+
+	/**
+	 * Creates a new {@link module:engine/view/text~Text} instance.
+	 *
+	 * @param {String} data The text's data.
+	 * @returns {module:engine/view/text~Text} The created text node.
+	 */
+	createText( data ) {
+		return new Text( data );
+	}
+
 	/**
 	 * Clones the provided element.
 	 *

+ 25 - 21
packages/ckeditor5-engine/src/view/view.js

@@ -53,10 +53,10 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
  * * {@link module:engine/view/observer/compositionobserver~CompositionObserver}.
  *
- * This class also {@link module:engine/view/view~View#attachDomRoot bind DOM and View elements}.
+ * This class also {@link module:engine/view/view~View#attachDomRoot binds the DOM and the view elements}.
  *
- * If you do not need full DOM - View management, and want to only transform the tree of view elements to the DOM
- * elements you do not need this controller, you can use the {@link module:engine/view/domconverter~DomConverter DomConverter}.
+ * If you do not need full a DOM - view management, and only want to transform a tree of view elements to a tree of DOM
+ * elements you do not need this controller. You can use the {@link module:engine/view/domconverter~DomConverter DomConverter} instead.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -308,30 +308,32 @@ export default class View {
 	}
 
 	/**
-	 * Change method is the primary way of changing the view. You should use it to modify any node in the view tree.
-	 * It makes sure that after all changes are made view is rendered to DOM. It prevents situations when DOM is updated
-	 * when view state is not yet correct. It allows to nest calls one inside another and still perform single rendering
-	 * after all changes are applied.
+	 * The `change()` method is the primary way of changing the view. You should use it to modify any node in the view tree.
+	 * It makes sure that after all changes are made the view is rendered to the DOM. It prevents situations when the DOM is updated
+	 * when the view state is not yet correct. It allows to nest calls one inside another and still performs a single rendering
+	 * after all those changes are made. It also returns the return value of its callback.
 	 *
-	 *		view.change( writer => {
-	 *			writer.insert( position1, writer.createText( 'foo' ) );
+	 *		const text = view.change( writer => {
+	 *			const newText = writer.createText( 'foo' );
+	 *			writer.insert( position1, newText );
 	 *
 	 *			view.change( writer => {
 	 *				writer.insert( position2, writer.createText( 'bar' ) );
 	 *			} );
 	 *
 	 * 			writer.remove( range );
-	 *		} );
 	 *
-	 * Change block is executed immediately.
+	 * 			return newText;
+	 *		} );
 	 *
-	 * When the outermost change block is done and rendering to DOM is over it fires
-	 * {@link module:engine/view/view~View#event:render} event.
+	 * When the outermost change block is done and rendering to the DOM is over the
+	 * {@link module:engine/view/view~View#event:render `View#render`} event is fired.
 	 *
-	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when
-	 * change block is used after rendering to DOM has started.
+	 * This method throws a `applying-view-changes-on-rendering` error when
+	 * the change block is used after rendering to the DOM has started.
 	 *
 	 * @param {Function} callback Callback function which may modify the view.
+	 * @returns {*} Value returned by the callback.
 	 */
 	change( callback ) {
 		if ( this._renderingInProgress || this._postFixersInProgress ) {
@@ -343,25 +345,25 @@ export default class View {
 			 * * calling {@link #change} or {@link #render} during rendering process,
 			 * * calling {@link #change} or {@link #render} inside of
 			 *   {@link module:engine/view/document~Document#registerPostFixer post-fixer function}.
+			 *
+			 * @error cannot-change-view-tree
 			 */
 			throw new CKEditorError(
 				'cannot-change-view-tree: ' +
-				'Attempting to make changes to the view when it is in incorrect state: rendering or post-fixers are in progress. ' +
-				'This may cause some unexpected behaviour and inconsistency between the DOM and the view.'
+				'Attempting to make changes to the view when it is in an incorrect state: rendering or post-fixers are in progress. ' +
+				'This may cause some unexpected behavior and inconsistency between the DOM and the view.'
 			);
 		}
 
 		// Recursive call to view.change() method - execute listener immediately.
 		if ( this._ongoingChange ) {
-			callback( this._writer );
-
-			return;
+			return callback( this._writer );
 		}
 
 		// This lock will assure that all recursive calls to view.change() will end up in same block - one "render"
 		// event for all nested calls.
 		this._ongoingChange = true;
-		callback( this._writer );
+		const callbackResult = callback( this._writer );
 		this._ongoingChange = false;
 
 		// This lock is used by editing controller to render changes from outer most model.change() once. As plugins might call
@@ -373,6 +375,8 @@ export default class View {
 
 			this.fire( 'render' );
 		}
+
+		return callbackResult;
 	}
 
 	/**

+ 67 - 0
packages/ckeditor5-engine/tests/view/upcastwriter.js

@@ -3,7 +3,9 @@
  * For licensing, see LICENSE.md.
  */
 
+import DocumentFragment from '../../src/view/documentfragment';
 import Element from '../../src/view/element';
+import Text from '../../src/view/text';
 import UpcastWriter from '../../src/view/upcastwriter';
 import HtmlDataProcessor from '../../src/dataprocessor/htmldataprocessor';
 import ViewPosition from '../../src/view/position';
@@ -32,6 +34,71 @@ describe( 'UpcastWriter', () => {
 		view = dataprocessor.toView( html );
 	} );
 
+	describe( 'createDocumentFragment', () => {
+		it( 'should create empty document fragment', () => {
+			const df = writer.createDocumentFragment();
+
+			expect( df ).to.instanceOf( DocumentFragment );
+			expect( df.childCount ).to.equal( 0 );
+		} );
+
+		it( 'should create document fragment with children', () => {
+			const df = writer.createDocumentFragment( [ view.getChild( 0 ), view.getChild( 1 ) ] );
+
+			expect( df ).to.instanceOf( DocumentFragment );
+			expect( df.childCount ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'createElement', () => {
+		it( 'should create empty element', () => {
+			const el = writer.createElement( 'p' );
+
+			expect( el ).to.instanceOf( Element );
+			expect( el.name ).to.equal( 'p' );
+			expect( Array.from( el.getAttributes() ).length ).to.equal( 0 );
+			expect( el.childCount ).to.equal( 0 );
+		} );
+
+		it( 'should create element with attributes', () => {
+			const el = writer.createElement( 'a', { 'class': 'editor', 'contentEditable': 'true' } );
+
+			expect( el ).to.instanceOf( Element );
+			expect( el.name ).to.equal( 'a' );
+			expect( Array.from( el.getAttributes() ).length ).to.equal( 2 );
+			expect( el.childCount ).to.equal( 0 );
+		} );
+
+		it( 'should create element with children', () => {
+			const el = writer.createElement( 'div', null, [ view.getChild( 0 ) ] );
+
+			expect( el ).to.instanceOf( Element );
+			expect( el.name ).to.equal( 'div' );
+			expect( Array.from( el.getAttributes() ).length ).to.equal( 0 );
+			expect( el.childCount ).to.equal( 1 );
+		} );
+
+		it( 'should create element with attributes and children', () => {
+			const el = writer.createElement( 'blockquote',
+				{ 'class': 'editor', 'contentEditable': 'true' },
+				view.getChild( 2 ) );
+
+			expect( el ).to.instanceOf( Element );
+			expect( el.name ).to.equal( 'blockquote' );
+			expect( Array.from( el.getAttributes() ).length ).to.equal( 2 );
+			expect( el.childCount ).to.equal( 1 );
+		} );
+	} );
+
+	describe( 'createText', () => {
+		it( 'should create text', () => {
+			const text = writer.createText( 'FooBar' );
+
+			expect( text ).to.instanceOf( Text );
+			expect( text.data ).to.equal( 'FooBar' );
+		} );
+	} );
+
 	describe( 'clone', () => {
 		it( 'should clone simple element', () => {
 			const el = view.getChild( 0 );

+ 28 - 0
packages/ckeditor5-engine/tests/view/view/view.js

@@ -602,6 +602,34 @@ describe( 'view', () => {
 
 			sinon.assert.callOrder( changeSpy, postFixer1, postFixer2, eventSpy );
 		} );
+
+		it( 'should return result of the callback', () => {
+			const result = view.change( () => {
+				return 'FooBar';
+			} );
+
+			expect( result ).to.equal( 'FooBar' );
+		} );
+
+		it( 'should return result of the callback with nested change block', () => {
+			let result2 = false;
+			let result3 = false;
+
+			const result1 = view.change( () => {
+				return view.change( () => {
+					result2 = view.change( () => {
+						return true;
+					} );
+					result3 = view.change( () => {} );
+
+					return 42;
+				} );
+			} );
+
+			expect( result1 ).to.equal( 42 );
+			expect( result2 ).to.equal( true );
+			expect( result3 ).to.undefined;
+		} );
 	} );
 
 	describe( 'createPositionAt()', () => {