From eb76e20bbd1d80543a652edc7f6f5caab2a01f18 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Thu, 11 Jan 2018 13:16:43 +0100
Subject: [PATCH 01/89] Changed wirter utility functions to class methods.
---
src/view/writer.js | 1333 ++++++++++++++++++++++----------------------
1 file changed, 658 insertions(+), 675 deletions(-)
diff --git a/src/view/writer.js b/src/view/writer.js
index 07aa9097a..e002b39b6 100644
--- a/src/view/writer.js
+++ b/src/view/writer.js
@@ -18,662 +18,773 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import DocumentFragment from './documentfragment';
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
-/**
- * Contains functions used for composing view tree.
- *
- * @namespace writer
- */
-
-const writer = {
- breakAttributes,
- breakContainer,
- mergeAttributes,
- mergeContainers,
- insert,
- remove,
- clear,
- move,
- wrap,
- wrapPosition,
- unwrap,
- rename
-};
-
-export default writer;
-
-/**
- * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
- * up to a container element.
- *
- * In following examples `
` is a container, `` and `` are attribute nodes:
- *
- *
foobar{}
->
foobar[]
- *
foo{}bar
->
foo{}bar
- *
foob{}ar
->
foob[]ar
- *
fo{oba}r
->
foobar
- *
- * **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
- *
- * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
- * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first
- * encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given
- * `position`
- * is directly in container element and breaks that container element.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container`
- * when {@link module:engine/view/range~Range#start start}
- * and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element`
- * when trying to break attributes
- * inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element`
- * when trying to break attributes
- * inside {@link module:engine/view/uielement~UIElement UIElement}.
- *
- * @see module:engine/view/attributeelement~AttributeElement
- * @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.breakContainer
- * @function module:engine/view/writer~writer.breakAttributes
- * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where to break attribute elements.
- * @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute
- * elements.
- */
-export function breakAttributes( positionOrRange ) {
- if ( positionOrRange instanceof Position ) {
- return _breakAttributes( positionOrRange );
- } else {
- return _breakAttributesRange( positionOrRange );
+// TODO: check all docs
+// TODO: writer should be protected
+// TODO: check errors/event descriptions if everything is up to date
+export default class Writer {
+ /**
+ * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
+ * up to a container element.
+ *
+ * In following examples `
` is a container, `` and `` are attribute nodes:
+ *
+ *
foobar{}
->
foobar[]
+ *
foo{}bar
->
foo{}bar
+ *
foob{}ar
->
foob[]ar
+ *
fo{oba}r
->
foobar
+ *
+ * **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
+ *
+ * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
+ * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
+ * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
+ * `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container`
+ * when {@link module:engine/view/range~Range#start start}
+ * and {@link module:engine/view/range~Range#end end} positions of a passed range are not placed inside same parent container.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-empty-element`
+ * when trying to break attributes
+ * inside {@link module:engine/view/emptyelement~EmptyElement EmptyElement}.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-cannot-break-ui-element`
+ * when trying to break attributes
+ * inside {@link module:engine/view/uielement~UIElement UIElement}.
+ *
+ * @see module:engine/view/attributeelement~AttributeElement
+ * @see module:engine/view/containerelement~ContainerElement
+ * @see module:engine/view/writer~writer.breakContainer
+ * @function module:engine/view/writer~writer.breakAttributes
+ * @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where to break
+ * attribute elements.
+ * @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute
+ * elements.
+ */
+ breakAttributes( positionOrRange ) {
+ if ( positionOrRange instanceof Position ) {
+ return _breakAttributes( positionOrRange );
+ } else {
+ return _breakAttributesRange( positionOrRange );
+ }
}
-}
-/**
- * Breaks {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position. Position
- * has to be directly inside container element and cannot be in root. Does not break if position is at the beginning
- * or at the end of it's parent element.
- *
- *
foo^bar
->
foo
bar
- *
foo
^
bar
->
foo
bar
- *
^foobar
-> ^
foobar
- *
foobar^
->
foobar
^
- *
- * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
- * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`, up to the first
- * encountered {@link module:engine/view/containerelement~ContainerElement container element}. `breakContainer` assumes that given
- * `position`
- * is directly in container element and breaks that container element.
- *
- * @see module:engine/view/attributeelement~AttributeElement
- * @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.breakAttributes
- * @function module:engine/view/writer~writer.breakContainer
- * @param {module:engine/view/position~Position} position Position where to break element.
- * @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken, the returned position
- * is placed either before it or after it.
- */
-export function breakContainer( position ) {
- const element = position.parent;
+ /**
+ * Breaks {@link module:engine/view/containerelement~ContainerElement container view element} into two, at the given position. Position
+ * has to be directly inside container element and cannot be in root. Does not break if position is at the beginning
+ * or at the end of it's parent element.
+ *
+ *
foo^bar
->
foo
bar
+ *
foo
^
bar
->
foo
bar
+ *
^foobar
-> ^
foobar
+ *
foobar^
->
foobar
^
+ *
+ * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
+ * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
+ * up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
+ * `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
+ *
+ * @see module:engine/view/attributeelement~AttributeElement
+ * @see module:engine/view/containerelement~ContainerElement
+ * @see module:engine/view/writer~writer.breakAttributes
+ * @function module:engine/view/writer~writer.breakContainer
+ * @param {module:engine/view/position~Position} position Position where to break element.
+ * @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken,
+ * the returned position is placed either before it or after it.
+ */
+ breakContainer( position ) {
+ const element = position.parent;
+
+ if ( !( element.is( 'containerElement' ) ) ) {
+ /**
+ * Trying to break an element which is not a container element.
+ *
+ * @error view-writer-break-non-container-element
+ */
+ throw new CKEditorError(
+ 'view-writer-break-non-container-element: Trying to break an element which is not a container element.'
+ );
+ }
- if ( !( element.is( 'containerElement' ) ) ) {
- /**
- * Trying to break an element which is not a container element.
- *
- * @error view-writer-break-non-container-element
- */
- throw new CKEditorError( 'view-writer-break-non-container-element: Trying to break an element which is not a container element.' );
- }
+ if ( !element.parent ) {
+ /**
+ * Trying to break root element.
+ *
+ * @error view-writer-break-root
+ */
+ throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
+ }
- if ( !element.parent ) {
- /**
- * Trying to break root element.
- *
- * @error view-writer-break-root
- */
- throw new CKEditorError( 'view-writer-break-root: Trying to break root element.' );
- }
+ if ( position.isAtStart ) {
+ return Position.createBefore( element );
+ } else if ( !position.isAtEnd ) {
+ const newElement = element.clone( false );
- if ( position.isAtStart ) {
- return Position.createBefore( element );
- } else if ( !position.isAtEnd ) {
- const newElement = element.clone( false );
+ this.insert( Position.createAfter( element ), newElement );
- insert( Position.createAfter( element ), newElement );
+ const sourceRange = new Range( position, Position.createAt( element, 'end' ) );
+ const targetPosition = new Position( newElement, 0 );
- const sourceRange = new Range( position, Position.createAt( element, 'end' ) );
- const targetPosition = new Position( newElement, 0 );
+ this.move( sourceRange, targetPosition );
+ }
- move( sourceRange, targetPosition );
+ return Position.createAfter( element );
}
- return Position.createAfter( element );
-}
+ /**
+ * Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
+ * Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
+ *
+ * In following examples `
` is a container and `` is an attribute element:
+ *
+ *
foo[]bar
->
foo{}bar
+ *
foo[]bar
->
foo{}bar
+ *
a[]b
->
a[]b
+ *
+ * It will also take care about empty attributes when merging:
+ *
+ *
[]
->
[]
+ *
foo[]bar
->
foo{}bar
+ *
+ * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
+ * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
+ * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
+ *
+ * @see module:engine/view/attributeelement~AttributeElement
+ * @see module:engine/view/containerelement~ContainerElement
+ * @see module:engine/view/writer~writer.mergeContainers
+ * @function module:engine/view/writer~writer.mergeAttributes
+ * @param {module:engine/view/position~Position} position Merge position.
+ * @returns {module:engine/view/position~Position} Position after merge.
+ */
+ mergeAttributes( position ) {
+ const positionOffset = position.offset;
+ const positionParent = position.parent;
+
+ // When inside text node - nothing to merge.
+ if ( positionParent.is( 'text' ) ) {
+ return position;
+ }
-/**
- * Merges {@link module:engine/view/attributeelement~AttributeElement attribute elements}. It also merges text nodes if needed.
- * Only {@link module:engine/view/attributeelement~AttributeElement#isSimilar similar} attribute elements can be merged.
- *
- * In following examples `
` is a container and `` is an attribute element:
- *
- *
foo[]bar
->
foo{}bar
- *
foo[]bar
->
foo{}bar
- *
a[]b
->
a[]b
- *
- * It will also take care about empty attributes when merging:
- *
- *
[]
->
[]
- *
foo[]bar
->
foo{}bar
- *
- * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
- * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
- * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
- *
- * @see module:engine/view/attributeelement~AttributeElement
- * @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.mergeContainers
- * @function module:engine/view/writer~writer.mergeAttributes
- * @param {module:engine/view/position~Position} position Merge position.
- * @returns {module:engine/view/position~Position} Position after merge.
- */
-export function mergeAttributes( position ) {
- const positionOffset = position.offset;
- const positionParent = position.parent;
+ // When inside empty attribute - remove it.
+ if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
+ const parent = positionParent.parent;
+ const offset = positionParent.index;
+ positionParent.remove();
- // When inside text node - nothing to merge.
- if ( positionParent.is( 'text' ) ) {
- return position;
- }
+ return this.mergeAttributes( new Position( parent, offset ) );
+ }
- // When inside empty attribute - remove it.
- if ( positionParent.is( 'attributeElement' ) && positionParent.childCount === 0 ) {
- const parent = positionParent.parent;
- const offset = positionParent.index;
- positionParent.remove();
+ const nodeBefore = positionParent.getChild( positionOffset - 1 );
+ const nodeAfter = positionParent.getChild( positionOffset );
- return mergeAttributes( new Position( parent, offset ) );
- }
+ // Position should be placed between two nodes.
+ if ( !nodeBefore || !nodeAfter ) {
+ return position;
+ }
- const nodeBefore = positionParent.getChild( positionOffset - 1 );
- const nodeAfter = positionParent.getChild( positionOffset );
+ // When position is between two text nodes.
+ if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
+ return mergeTextNodes( nodeBefore, nodeAfter );
+ }
+ // When selection is between two same attribute elements.
+ else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
+ // Move all children nodes from node placed after selection and remove that node.
+ const count = nodeBefore.childCount;
+ nodeBefore.appendChildren( nodeAfter.getChildren() );
+ nodeAfter.remove();
+
+ // New position is located inside the first node, before new nodes.
+ // Call this method recursively to merge again if needed.
+ return this.mergeAttributes( new Position( nodeBefore, count ) );
+ }
- // Position should be placed between two nodes.
- if ( !nodeBefore || !nodeAfter ) {
return position;
}
- // When position is between two text nodes.
- if ( nodeBefore.is( 'text' ) && nodeAfter.is( 'text' ) ) {
- return mergeTextNodes( nodeBefore, nodeAfter );
- }
- // When selection is between two same attribute elements.
- else if ( nodeBefore.is( 'attributeElement' ) && nodeAfter.is( 'attributeElement' ) && nodeBefore.isSimilar( nodeAfter ) ) {
- // Move all children nodes from node placed after selection and remove that node.
- const count = nodeBefore.childCount;
- nodeBefore.appendChildren( nodeAfter.getChildren() );
- nodeAfter.remove();
-
- // New position is located inside the first node, before new nodes.
- // Call this method recursively to merge again if needed.
- return mergeAttributes( new Position( nodeBefore, count ) );
- }
+ /**
+ * Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
+ * Precisely, the element after the position is removed and it's contents are moved to element before the position.
+ *
+ *
foo
^
bar
->
foo^bar
+ *
foo
^
bar
->
foo^bar
+ *
+ * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
+ * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
+ * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
+ *
+ * @see module:engine/view/attributeelement~AttributeElement
+ * @see module:engine/view/containerelement~ContainerElement
+ * @see module:engine/view/writer~writer.mergeAttributes
+ * @function module:engine/view/writer~writer.mergeContainers
+ * @param {module:engine/view/position~Position} position Merge position.
+ * @returns {module:engine/view/position~Position} Position after merge.
+ */
+ mergeContainers( position ) {
+ const prev = position.nodeBefore;
+ const next = position.nodeAfter;
+
+ if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
+ /**
+ * Element before and after given position cannot be merged.
+ *
+ * @error view-writer-merge-containers-invalid-position
+ */
+ throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
+ 'Element before and after given position cannot be merged.' );
+ }
- return position;
-}
+ const lastChild = prev.getChild( prev.childCount - 1 );
+ const newPosition = lastChild instanceof Text ? Position.createAt( lastChild, 'end' ) : Position.createAt( prev, 'end' );
-/**
- * Merges two {@link module:engine/view/containerelement~ContainerElement container elements} that are before and after given position.
- * Precisely, the element after the position is removed and it's contents are moved to element before the position.
- *
- *
foo
^
bar
->
foo^bar
- *
foo
^
bar
->
foo^bar
- *
- * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
- * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
- * {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
- * while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
- *
- * @see module:engine/view/attributeelement~AttributeElement
- * @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.mergeAttributes
- * @function module:engine/view/writer~writer.mergeContainers
- * @param {module:engine/view/position~Position} position Merge position.
- * @returns {module:engine/view/position~Position} Position after merge.
- */
-export function mergeContainers( position ) {
- const prev = position.nodeBefore;
- const next = position.nodeAfter;
+ this.move( Range.createIn( next ), Position.createAt( prev, 'end' ) );
+ this.remove( Range.createOn( next ) );
- if ( !prev || !next || !prev.is( 'containerElement' ) || !next.is( 'containerElement' ) ) {
- /**
- * Element before and after given position cannot be merged.
- *
- * @error view-writer-merge-containers-invalid-position
- */
- throw new CKEditorError( 'view-writer-merge-containers-invalid-position: ' +
- 'Element before and after given position cannot be merged.' );
+ return newPosition;
}
- const lastChild = prev.getChild( prev.childCount - 1 );
- const newPosition = lastChild instanceof Text ? Position.createAt( lastChild, 'end' ) : Position.createAt( prev, 'end' );
+ /**
+ * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
+ * and merging them afterwards.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
+ * contains instances that are not {@link module:engine/view/text~Text Texts},
+ * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
+ * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
+ * {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
+ * {@link module:engine/view/uielement~UIElement UIElements}.
+ *
+ * @function insert
+ * @param {module:engine/view/position~Position} position Insertion position.
+ * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
+ * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
+ * module:engine/view/uielement~UIElement|Iterable.} nodes Node or nodes to insert.
+ * @returns {module:engine/view/range~Range} Range around inserted nodes.
+ */
+ insert( position, nodes ) {
+ nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
+
+ // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
+ validateNodesToInsert( nodes );
+
+ const container = getParentContainer( position );
+
+ if ( !container ) {
+ /**
+ * Position's parent container cannot be found.
+ *
+ * @error view-writer-invalid-position-container
+ */
+ throw new CKEditorError( 'view-writer-invalid-position-container' );
+ }
- move( Range.createIn( next ), Position.createAt( prev, 'end' ) );
- remove( Range.createOn( next ) );
+ const insertionPosition = _breakAttributes( position, true );
- return newPosition;
-}
+ const length = container.insertChildren( insertionPosition.offset, nodes );
+ const endPosition = insertionPosition.getShiftedBy( length );
+ const start = this.mergeAttributes( insertionPosition );
-/**
- * Insert node or nodes at specified position. Takes care about breaking attributes before insertion
- * and merging them afterwards.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-insert-invalid-node` when nodes to insert
- * contains instances that are not {@link module:engine/view/text~Text Texts},
- * {@link module:engine/view/attributeelement~AttributeElement AttributeElements},
- * {@link module:engine/view/containerelement~ContainerElement ContainerElements},
- * {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
- * {@link module:engine/view/uielement~UIElement UIElements}.
- *
- * @function insert
- * @param {module:engine/view/position~Position} position Insertion position.
- * @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
- * module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
- * module:engine/view/uielement~UIElement|Iterable.} nodes Node or nodes to insert.
- * @returns {module:engine/view/range~Range} Range around inserted nodes.
- */
-export function insert( position, nodes ) {
- nodes = isIterable( nodes ) ? [ ...nodes ] : [ nodes ];
-
- // Check if nodes to insert are instances of AttributeElements, ContainerElements, EmptyElements, UIElements or Text.
- validateNodesToInsert( nodes );
+ // When no nodes were inserted - return collapsed range.
+ if ( length === 0 ) {
+ return new Range( start, start );
+ } else {
+ // If start position was merged - move end position.
+ if ( !start.isEqual( insertionPosition ) ) {
+ endPosition.offset--;
+ }
- const container = getParentContainer( position );
+ const end = this.mergeAttributes( endPosition );
- if ( !container ) {
- /**
- * Position's parent container cannot be found.
- *
- * @error view-writer-invalid-position-container
- */
- throw new CKEditorError( 'view-writer-invalid-position-container' );
+ return new Range( start, end );
+ }
}
- const insertionPosition = _breakAttributes( position, true );
+ /**
+ * Removes provided range from the container.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
+ * same parent container.
+ *
+ * @function module:engine/view/writer~writer.remove
+ * @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
+ * to a collapsed range showing the new position.
+ * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
+ */
+ remove( range ) {
+ validateRangeContainer( range );
+
+ // If range is collapsed - nothing to remove.
+ if ( range.isCollapsed ) {
+ return new DocumentFragment();
+ }
+
+ // Break attributes at range start and end.
+ const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
+ const parentContainer = breakStart.parent;
- const length = container.insertChildren( insertionPosition.offset, nodes );
- const endPosition = insertionPosition.getShiftedBy( length );
- const start = mergeAttributes( insertionPosition );
+ const count = breakEnd.offset - breakStart.offset;
- // When no nodes were inserted - return collapsed range.
- if ( length === 0 ) {
- return new Range( start, start );
- } else {
- // If start position was merged - move end position.
- if ( !start.isEqual( insertionPosition ) ) {
- endPosition.offset--;
- }
+ // Remove nodes in range.
+ const removed = parentContainer.removeChildren( breakStart.offset, count );
- const end = mergeAttributes( endPosition );
+ // Merge after removing.
+ const mergePosition = this.mergeAttributes( breakStart );
+ range.start = mergePosition;
+ range.end = Position.createFromPosition( mergePosition );
- return new Range( start, end );
+ // Return removed nodes.
+ return new DocumentFragment( removed );
}
-}
-/**
- * Removes provided range from the container.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
- * same parent container.
- *
- * @function module:engine/view/writer~writer.remove
- * @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
- * to a collapsed range showing the new position.
- * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
- */
-export function remove( range ) {
- validateRangeContainer( range );
+ /**
+ * Removes matching elements from given range.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
+ * same parent container.
+ *
+ * @function module:engine/view/writer~writer.clear
+ * @param {module:engine/view/range~Range} range Range to clear.
+ * @param {module:engine/view/element~Element} element Element to remove.
+ */
+ clear( range, element ) {
+ validateRangeContainer( range );
+
+ // Create walker on given range.
+ // We walk backward because when we remove element during walk it modifies range end position.
+ const walker = range.getWalker( {
+ direction: 'backward',
+ ignoreElementEnd: true
+ } );
+
+ // Let's walk.
+ for ( const current of walker ) {
+ const item = current.item;
+ let rangeToRemove;
+
+ // When current item matches to the given element.
+ if ( item.is( 'element' ) && element.isSimilar( item ) ) {
+ // Create range on this element.
+ rangeToRemove = Range.createOn( item );
+ // When range starts inside Text or TextProxy element.
+ } else if ( !current.nextPosition.isAfter( range.start ) && item.is( 'textProxy' ) ) {
+ // We need to check if parent of this text matches to given element.
+ const parentElement = item.getAncestors().find( ancestor => {
+ return ancestor.is( 'element' ) && element.isSimilar( ancestor );
+ } );
+
+ // If it is then create range inside this element.
+ if ( parentElement ) {
+ rangeToRemove = Range.createIn( parentElement );
+ }
+ }
- // If range is collapsed - nothing to remove.
- if ( range.isCollapsed ) {
- return new DocumentFragment();
- }
+ // If we have found element to remove.
+ if ( rangeToRemove ) {
+ // We need to check if element range stick out of the given range and truncate if it is.
+ if ( rangeToRemove.end.isAfter( range.end ) ) {
+ rangeToRemove.end = range.end;
+ }
- // Break attributes at range start and end.
- const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
- const parentContainer = breakStart.parent;
+ if ( rangeToRemove.start.isBefore( range.start ) ) {
+ rangeToRemove.start = range.start;
+ }
- const count = breakEnd.offset - breakStart.offset;
+ // At the end we remove range with found element.
+ this.remove( rangeToRemove );
+ }
+ }
+ }
- // Remove nodes in range.
- const removed = parentContainer.removeChildren( breakStart.offset, count );
+ /**
+ * Moves nodes from provided range to target position.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
+ * same parent container.
+ *
+ * @function module:engine/view/writer~writer.move
+ * @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
+ * @param {module:engine/view/position~Position} targetPosition Position to insert.
+ * @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
+ */
+ move( sourceRange, targetPosition ) {
+ let nodes;
+
+ if ( targetPosition.isAfter( sourceRange.end ) ) {
+ targetPosition = _breakAttributes( targetPosition, true );
+
+ const parent = targetPosition.parent;
+ const countBefore = parent.childCount;
+
+ sourceRange = _breakAttributesRange( sourceRange, true );
+
+ nodes = this.remove( sourceRange );
+
+ targetPosition.offset += ( parent.childCount - countBefore );
+ } else {
+ nodes = this.remove( sourceRange );
+ }
- // Merge after removing.
- const mergePosition = mergeAttributes( breakStart );
- range.start = mergePosition;
- range.end = Position.createFromPosition( mergePosition );
+ return this.insert( targetPosition, nodes );
+ }
- // Return removed nodes.
- return new DocumentFragment( removed );
-}
+ /**
+ * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
+ * when {@link module:engine/view/range~Range#start}
+ * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
+ * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
+ *
+ * @function module:engine/view/writer~writer.wrap
+ * @param {module:engine/view/range~Range} range Range to wrap.
+ * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
+ */
+ wrap( range, attribute ) {
+ if ( !( attribute instanceof AttributeElement ) ) {
+ throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
+ }
-/**
- * Removes matching elements from given range.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
- * same parent container.
- *
- * @function module:engine/view/writer~writer.clear
- * @param {module:engine/view/range~Range} range Range to clear.
- * @param {module:engine/view/element~Element} element Element to remove.
- */
-export function clear( range, element ) {
- validateRangeContainer( range );
+ validateRangeContainer( range );
- // Create walker on given range.
- // We walk backward because when we remove element during walk it modifies range end position.
- const walker = range.getWalker( {
- direction: 'backward',
- ignoreElementEnd: true
- } );
-
- // Let's walk.
- for ( const current of walker ) {
- const item = current.item;
- let rangeToRemove;
-
- // When current item matches to the given element.
- if ( item.is( 'element' ) && element.isSimilar( item ) ) {
- // Create range on this element.
- rangeToRemove = Range.createOn( item );
- // When range starts inside Text or TextProxy element.
- } else if ( !current.nextPosition.isAfter( range.start ) && item.is( 'textProxy' ) ) {
- // We need to check if parent of this text matches to given element.
- const parentElement = item.getAncestors().find( ancestor => {
- return ancestor.is( 'element' ) && element.isSimilar( ancestor );
- } );
-
- // If it is then create range inside this element.
- if ( parentElement ) {
- rangeToRemove = Range.createIn( parentElement );
- }
+ // If range is collapsed - nothing to wrap.
+ if ( range.isCollapsed ) {
+ return range;
}
- // If we have found element to remove.
- if ( rangeToRemove ) {
- // We need to check if element range stick out of the given range and truncate if it is.
- if ( rangeToRemove.end.isAfter( range.end ) ) {
- rangeToRemove.end = range.end;
- }
+ // Range is inside single attribute and spans on all children.
+ if ( rangeSpansOnAllChildren( range ) && wrapAttributeElement( attribute, range.start.parent ) ) {
+ const parent = range.start.parent;
- if ( rangeToRemove.start.isBefore( range.start ) ) {
- rangeToRemove.start = range.start;
- }
+ const end = this.mergeAttributes( Position.createAfter( parent ) );
+ const start = this.mergeAttributes( Position.createBefore( parent ) );
- // At the end we remove range with found element.
- remove( rangeToRemove );
+ return new Range( start, end );
}
- }
-}
-/**
- * Moves nodes from provided range to target position.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
- * same parent container.
- *
- * @function module:engine/view/writer~writer.move
- * @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
- * @param {module:engine/view/position~Position} targetPosition Position to insert.
- * @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions.
- */
-export function move( sourceRange, targetPosition ) {
- let nodes;
+ // Break attributes at range start and end.
+ const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
- if ( targetPosition.isAfter( sourceRange.end ) ) {
- targetPosition = _breakAttributes( targetPosition, true );
+ // Range around one element.
+ if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
+ const node = breakStart.nodeAfter;
- const parent = targetPosition.parent;
- const countBefore = parent.childCount;
+ if ( node instanceof AttributeElement && wrapAttributeElement( attribute, node ) ) {
+ const start = this.mergeAttributes( breakStart );
- sourceRange = _breakAttributesRange( sourceRange, true );
+ if ( !start.isEqual( breakStart ) ) {
+ breakEnd.offset--;
+ }
- nodes = remove( sourceRange );
+ const end = this.mergeAttributes( breakEnd );
- targetPosition.offset += ( parent.childCount - countBefore );
- } else {
- nodes = remove( sourceRange );
- }
+ return new Range( start, end );
+ }
+ }
- return insert( targetPosition, nodes );
-}
+ const parentContainer = breakStart.parent;
-/**
- * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container`
- * when {@link module:engine/view/range~Range#start}
- * and {@link module:engine/view/range~Range#end} positions are not placed inside same parent container.
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
- * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
- *
- * @function module:engine/view/writer~writer.wrap
- * @param {module:engine/view/range~Range} range Range to wrap.
- * @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
- */
-export function wrap( range, attribute ) {
- if ( !( attribute instanceof AttributeElement ) ) {
- throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
- }
-
- validateRangeContainer( range );
+ // Unwrap children located between break points.
+ const unwrappedRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
- // If range is collapsed - nothing to wrap.
- if ( range.isCollapsed ) {
- return range;
- }
+ // Wrap all children with attribute.
+ const newRange = this._wrapChildren( parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
- // Range is inside single attribute and spans on all children.
- if ( rangeSpansOnAllChildren( range ) && wrapAttributeElement( attribute, range.start.parent ) ) {
- const parent = range.start.parent;
+ // Merge attributes at the both ends and return a new range.
+ const start = this.mergeAttributes( newRange.start );
- const end = mergeAttributes( Position.createAfter( parent ) );
- const start = mergeAttributes( Position.createBefore( parent ) );
+ // If start position was merged - move end position back.
+ if ( !start.isEqual( newRange.start ) ) {
+ newRange.end.offset--;
+ }
+ const end = this.mergeAttributes( newRange.end );
return new Range( start, end );
}
- // Break attributes at range start and end.
- const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
+ /**
+ * Wraps position with provided attribute. Returns new position after wrapping. This method will also merge newly
+ * added attribute with its siblings whenever possible.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
+ * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
+ *
+ * @param {module:engine/view/position~Position} position
+ * @param {module:engine/view/attributeelement~AttributeElement} attribute
+ * @returns {module:engine/view/position~Position} New position after wrapping.
+ */
+ wrapPosition( position, attribute ) {
+ if ( !( attribute instanceof AttributeElement ) ) {
+ throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
+ }
- // Range around one element.
- if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
- const node = breakStart.nodeAfter;
+ // Return same position when trying to wrap with attribute similar to position parent.
+ if ( attribute.isSimilar( position.parent ) ) {
+ return movePositionToTextNode( Position.createFromPosition( position ) );
+ }
- if ( node instanceof AttributeElement && wrapAttributeElement( attribute, node ) ) {
- const start = mergeAttributes( breakStart );
+ // When position is inside text node - break it and place new position between two text nodes.
+ if ( position.parent.is( 'text' ) ) {
+ position = breakTextNode( position );
+ }
- if ( !start.isEqual( breakStart ) ) {
- breakEnd.offset--;
- }
+ // Create fake element that will represent position, and will not be merged with other attributes.
+ const fakePosition = new AttributeElement();
+ fakePosition.priority = Number.POSITIVE_INFINITY;
+ fakePosition.isSimilar = () => false;
- const end = mergeAttributes( breakEnd );
+ // Insert fake element in position location.
+ position.parent.insertChildren( position.offset, fakePosition );
- return new Range( start, end );
- }
- }
+ // Range around inserted fake attribute element.
+ const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
- const parentContainer = breakStart.parent;
+ // Wrap fake element with attribute (it will also merge if possible).
+ this.wrap( wrapRange, attribute );
- // Unwrap children located between break points.
- const unwrappedRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
+ // Remove fake element and place new position there.
+ const newPosition = new Position( fakePosition.parent, fakePosition.index );
+ fakePosition.remove();
- // Wrap all children with attribute.
- const newRange = wrapChildren( parentContainer, unwrappedRange.start.offset, unwrappedRange.end.offset, attribute );
+ // If position is placed between text nodes - merge them and return position inside.
+ const nodeBefore = newPosition.nodeBefore;
+ const nodeAfter = newPosition.nodeAfter;
- // Merge attributes at the both ends and return a new range.
- const start = mergeAttributes( newRange.start );
+ if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
+ return mergeTextNodes( nodeBefore, nodeAfter );
+ }
- // If start position was merged - move end position back.
- if ( !start.isEqual( newRange.start ) ) {
- newRange.end.offset--;
+ // If position is next to text node - move position inside.
+ return movePositionToTextNode( newPosition );
}
- const end = mergeAttributes( newRange.end );
- return new Range( start, end );
-}
+ /**
+ * Unwraps nodes within provided range from attribute element.
+ *
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
+ * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
+ * same parent container.
+ *
+ * @param {module:engine/view/range~Range} range
+ * @param {module:engine/view/attributeelement~AttributeElement} attribute
+ */
+ unwrap( range, attribute ) {
+ if ( !( attribute instanceof AttributeElement ) ) {
+ /**
+ * Attribute element need to be instance of attribute element.
+ *
+ * @error view-writer-unwrap-invalid-attribute
+ */
+ throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' );
+ }
-/**
- * Wraps position with provided attribute. Returns new position after wrapping. This method will also merge newly
- * added attribute with its siblings whenever possible.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
- * an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
- *
- * @param {module:engine/view/position~Position} position
- * @param {module:engine/view/attributeelement~AttributeElement} attribute
- * @returns {module:engine/view/position~Position} New position after wrapping.
- */
-export function wrapPosition( position, attribute ) {
- if ( !( attribute instanceof AttributeElement ) ) {
- throw new CKEditorError( 'view-writer-wrap-invalid-attribute' );
- }
+ validateRangeContainer( range );
- // Return same position when trying to wrap with attribute similar to position parent.
- if ( attribute.isSimilar( position.parent ) ) {
- return movePositionToTextNode( Position.createFromPosition( position ) );
- }
+ // If range is collapsed - nothing to unwrap.
+ if ( range.isCollapsed ) {
+ return range;
+ }
- // When position is inside text node - break it and place new position between two text nodes.
- if ( position.parent.is( 'text' ) ) {
- position = breakTextNode( position );
- }
+ // Break attributes at range start and end.
+ const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
- // Create fake element that will represent position, and will not be merged with other attributes.
- const fakePosition = new AttributeElement();
- fakePosition.priority = Number.POSITIVE_INFINITY;
- fakePosition.isSimilar = () => false;
+ // Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
+ // For example:
+ // unwrap with:
result:
+ if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
+ const node = breakStart.nodeAfter;
- // Insert fake element in position location.
- position.parent.insertChildren( position.offset, fakePosition );
+ // Unwrap single attribute element.
+ if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && unwrapAttributeElement( attribute, node ) ) {
+ const start = this.mergeAttributes( breakStart );
- // Range around inserted fake attribute element.
- const wrapRange = new Range( position, position.getShiftedBy( 1 ) );
+ if ( !start.isEqual( breakStart ) ) {
+ breakEnd.offset--;
+ }
- // Wrap fake element with attribute (it will also merge if possible).
- wrap( wrapRange, attribute );
+ const end = this.mergeAttributes( breakEnd );
- // Remove fake element and place new position there.
- const newPosition = new Position( fakePosition.parent, fakePosition.index );
- fakePosition.remove();
+ return new Range( start, end );
+ }
+ }
- // If position is placed between text nodes - merge them and return position inside.
- const nodeBefore = newPosition.nodeBefore;
- const nodeAfter = newPosition.nodeAfter;
+ const parentContainer = breakStart.parent;
- if ( nodeBefore instanceof Text && nodeAfter instanceof Text ) {
- return mergeTextNodes( nodeBefore, nodeAfter );
- }
+ // Unwrap children located between break points.
+ const newRange = this._unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
- // If position is next to text node - move position inside.
- return movePositionToTextNode( newPosition );
-}
+ // Merge attributes at the both ends and return a new range.
+ const start = this.mergeAttributes( newRange.start );
-/**
- * Unwraps nodes within provided range from attribute element.
- *
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `view-writer-invalid-range-container` when
- * {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
- * same parent container.
- *
- * @param {module:engine/view/range~Range} range
- * @param {module:engine/view/attributeelement~AttributeElement} attribute
- */
-export function unwrap( range, attribute ) {
- if ( !( attribute instanceof AttributeElement ) ) {
- /**
- * Attribute element need to be instance of attribute element.
- *
- * @error view-writer-unwrap-invalid-attribute
- */
- throw new CKEditorError( 'view-writer-unwrap-invalid-attribute' );
- }
+ // If start position was merged - move end position back.
+ if ( !start.isEqual( newRange.start ) ) {
+ newRange.end.offset--;
+ }
- validateRangeContainer( range );
+ const end = this.mergeAttributes( newRange.end );
- // If range is collapsed - nothing to unwrap.
- if ( range.isCollapsed ) {
- return range;
+ return new Range( start, end );
}
- // Break attributes at range start and end.
- const { start: breakStart, end: breakEnd } = _breakAttributesRange( range, true );
+ /**
+ * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
+ * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
+ * has renamed element as {@link module:engine/view/position~Position#parent a parent}.
+ *
+ * New element has to be created because `Element#tagName` property in DOM is readonly.
+ *
+ * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
+ *
+ * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
+ * @param {String} newName New name for element.
+ */
+ rename( viewElement, newName ) {
+ const newElement = new ContainerElement( newName, viewElement.getAttributes() );
+
+ this.insert( Position.createAfter( viewElement ), newElement );
+ this.move( Range.createIn( viewElement ), Position.createAt( newElement ) );
+ this.remove( Range.createOn( viewElement ) );
+
+ return newElement;
+ }
- // Range around one element - check if AttributeElement can be unwrapped partially when it's not similar.
- // For example:
- // unwrap with: result:
- if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) {
- const node = breakStart.nodeAfter;
+ // Unwraps children from provided `attribute`. Only children contained in `parent` element between
+ // `startOffset` and `endOffset` will be unwrapped.
+ //
+ // @param {module:engine/view/element~Element} parent
+ // @param {Number} startOffset
+ // @param {Number} endOffset
+ // @param {module:engine/view/element~Element} attribute
+ _unwrapChildren( parent, startOffset, endOffset, attribute ) {
+ let i = startOffset;
+ const unwrapPositions = [];
+
+ // Iterate over each element between provided offsets inside parent.
+ while ( i < endOffset ) {
+ const child = parent.getChild( i );
+
+ // If attributes are the similar, then unwrap.
+ if ( child.isSimilar( attribute ) ) {
+ const unwrapped = child.getChildren();
+ const count = child.childCount;
+
+ // Replace wrapper element with its children
+ child.remove();
+ parent.insertChildren( i, unwrapped );
+
+ // Save start and end position of moved items.
+ unwrapPositions.push(
+ new Position( parent, i ),
+ new Position( parent, i + count )
+ );
+
+ // Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
+ // elements.
+ i += count;
+ endOffset += count - 1;
+ } else {
+ // If other nested attribute is found start unwrapping there.
+ if ( child.is( 'attributeElement' ) ) {
+ this._unwrapChildren( child, 0, child.childCount, attribute );
+ }
+
+ i++;
+ }
+ }
- // Unwrap single attribute element.
- if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && unwrapAttributeElement( attribute, node ) ) {
- const start = mergeAttributes( breakStart );
+ // Merge at each unwrap.
+ let offsetChange = 0;
- if ( !start.isEqual( breakStart ) ) {
- breakEnd.offset--;
+ for ( const position of unwrapPositions ) {
+ position.offset -= offsetChange;
+
+ // Do not merge with elements outside selected children.
+ if ( position.offset == startOffset || position.offset == endOffset ) {
+ continue;
}
- const end = mergeAttributes( breakEnd );
+ const newPosition = this.mergeAttributes( position );
- return new Range( start, end );
+ // If nodes were merged - other merge offsets will change.
+ if ( !newPosition.isEqual( position ) ) {
+ offsetChange++;
+ endOffset--;
+ }
}
- }
- const parentContainer = breakStart.parent;
+ return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
+ }
- // Unwrap children located between break points.
- const newRange = unwrapChildren( parentContainer, breakStart.offset, breakEnd.offset, attribute );
+ // Wraps children with provided `attribute`. Only children contained in `parent` element between
+ // `startOffset` and `endOffset` will be wrapped.
+ //
+ // @param {module:engine/view/element~Element} parent
+ // @param {Number} startOffset
+ // @param {Number} endOffset
+ // @param {module:engine/view/element~Element} attribute
+ wrapChildren( parent, startOffset, endOffset, attribute ) {
+ let i = startOffset;
+ const wrapPositions = [];
+
+ while ( i < endOffset ) {
+ const child = parent.getChild( i );
+ const isText = child.is( 'text' );
+ const isAttribute = child.is( 'attributeElement' );
+ const isEmpty = child.is( 'emptyElement' );
+ const isUI = child.is( 'uiElement' );
+
+ // Wrap text, empty elements, ui elements or attributes with higher or equal priority.
+ if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
+ // Clone attribute.
+ const newAttribute = attribute.clone();
+
+ // Wrap current node with new attribute;
+ child.remove();
+ newAttribute.appendChildren( child );
+ parent.insertChildren( i, newAttribute );
+
+ wrapPositions.push( new Position( parent, i ) );
+ }
+ // If other nested attribute is found start wrapping there.
+ else if ( isAttribute ) {
+ this.wrapChildren( child, 0, child.childCount, attribute );
+ }
- // Merge attributes at the both ends and return a new range.
- const start = mergeAttributes( newRange.start );
+ i++;
+ }
- // If start position was merged - move end position back.
- if ( !start.isEqual( newRange.start ) ) {
- newRange.end.offset--;
- }
+ // Merge at each wrap.
+ let offsetChange = 0;
- const end = mergeAttributes( newRange.end );
+ for ( const position of wrapPositions ) {
+ position.offset -= offsetChange;
- return new Range( start, end );
-}
+ // Do not merge with elements outside selected children.
+ if ( position.offset == startOffset ) {
+ continue;
+ }
-/**
- * Renames element by creating a copy of renamed element but with changed name and then moving contents of the
- * old element to the new one. Keep in mind that this will invalidate all {@link module:engine/view/position~Position positions} which
- * has renamed element as {@link module:engine/view/position~Position#parent a parent}.
- *
- * New element has to be created because `Element#tagName` property in DOM is readonly.
- *
- * Since this function creates a new element and removes the given one, the new element is returned to keep reference.
- *
- * @param {module:engine/view/containerelement~ContainerElement} viewElement Element to be renamed.
- * @param {String} newName New name for element.
- */
-export function rename( viewElement, newName ) {
- const newElement = new ContainerElement( newName, viewElement.getAttributes() );
+ const newPosition = this.mergeAttributes( position );
- insert( Position.createAfter( viewElement ), newElement );
- move( Range.createIn( viewElement ), Position.createAt( newElement ) );
- remove( Range.createOn( viewElement ) );
+ // If nodes were merged - other merge offsets will change.
+ if ( !newPosition.isEqual( position ) ) {
+ offsetChange++;
+ endOffset--;
+ }
+ }
- return newElement;
+ return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
+ }
}
/**
@@ -831,134 +942,6 @@ function _breakAttributes( position, forceSplitText = false ) {
}
}
-// Unwraps children from provided `attribute`. Only children contained in `parent` element between
-// `startOffset` and `endOffset` will be unwrapped.
-//
-// @param {module:engine/view/element~Element} parent
-// @param {Number} startOffset
-// @param {Number} endOffset
-// @param {module:engine/view/element~Element} attribute
-function unwrapChildren( parent, startOffset, endOffset, attribute ) {
- let i = startOffset;
- const unwrapPositions = [];
-
- // Iterate over each element between provided offsets inside parent.
- while ( i < endOffset ) {
- const child = parent.getChild( i );
-
- // If attributes are the similar, then unwrap.
- if ( child.isSimilar( attribute ) ) {
- const unwrapped = child.getChildren();
- const count = child.childCount;
-
- // Replace wrapper element with its children
- child.remove();
- parent.insertChildren( i, unwrapped );
-
- // Save start and end position of moved items.
- unwrapPositions.push(
- new Position( parent, i ),
- new Position( parent, i + count )
- );
-
- // Skip elements that were unwrapped. Assuming that there won't be another element to unwrap in child
- // elements.
- i += count;
- endOffset += count - 1;
- } else {
- // If other nested attribute is found start unwrapping there.
- if ( child.is( 'attributeElement' ) ) {
- unwrapChildren( child, 0, child.childCount, attribute );
- }
-
- i++;
- }
- }
-
- // Merge at each unwrap.
- let offsetChange = 0;
-
- for ( const position of unwrapPositions ) {
- position.offset -= offsetChange;
-
- // Do not merge with elements outside selected children.
- if ( position.offset == startOffset || position.offset == endOffset ) {
- continue;
- }
-
- const newPosition = mergeAttributes( position );
-
- // If nodes were merged - other merge offsets will change.
- if ( !newPosition.isEqual( position ) ) {
- offsetChange++;
- endOffset--;
- }
- }
-
- return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
-}
-
-// Wraps children with provided `attribute`. Only children contained in `parent` element between
-// `startOffset` and `endOffset` will be wrapped.
-//
-// @param {module:engine/view/element~Element} parent
-// @param {Number} startOffset
-// @param {Number} endOffset
-// @param {module:engine/view/element~Element} attribute
-function wrapChildren( parent, startOffset, endOffset, attribute ) {
- let i = startOffset;
- const wrapPositions = [];
-
- while ( i < endOffset ) {
- const child = parent.getChild( i );
- const isText = child.is( 'text' );
- const isAttribute = child.is( 'attributeElement' );
- const isEmpty = child.is( 'emptyElement' );
- const isUI = child.is( 'uiElement' );
-
- // Wrap text, empty elements, ui elements or attributes with higher or equal priority.
- if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
- // Clone attribute.
- const newAttribute = attribute.clone();
-
- // Wrap current node with new attribute;
- child.remove();
- newAttribute.appendChildren( child );
- parent.insertChildren( i, newAttribute );
-
- wrapPositions.push( new Position( parent, i ) );
- }
- // If other nested attribute is found start wrapping there.
- else if ( isAttribute ) {
- wrapChildren( child, 0, child.childCount, attribute );
- }
-
- i++;
- }
-
- // Merge at each wrap.
- let offsetChange = 0;
-
- for ( const position of wrapPositions ) {
- position.offset -= offsetChange;
-
- // Do not merge with elements outside selected children.
- if ( position.offset == startOffset ) {
- continue;
- }
-
- const newPosition = mergeAttributes( position );
-
- // If nodes were merged - other merge offsets will change.
- if ( !newPosition.isEqual( position ) ) {
- offsetChange++;
- endOffset--;
- }
- }
-
- return Range.createFromParentsAndOffsets( parent, startOffset, parent, endOffset );
-}
-
// Checks if first {@link module:engine/view/attributeelement~AttributeElement AttributeElement} provided to the function
// can be wrapped otuside second element. It is done by comparing elements'
// {@link module:engine/view/attributeelement~AttributeElement#priority priorities}, if both have same priority
From 5639b6a74a70f6b6e6c5d0e5fd53171c46bf3718 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Thu, 11 Jan 2018 15:08:33 +0100
Subject: [PATCH 02/89] Fixed view writer tests.
---
src/view/writer.js | 4 +-
tests/view/writer/breakattributes.js | 28 +--
tests/view/writer/breakcontainer.js | 38 ++--
tests/view/writer/clear.js | 36 ++--
tests/view/writer/insert.js | 50 ++---
tests/view/writer/mergeattributes.js | 36 ++--
tests/view/writer/mergecontainers.js | 46 +++--
tests/view/writer/move.js | 54 +++---
tests/view/writer/remove.js | 54 +++---
tests/view/writer/rename.js | 16 +-
tests/view/writer/unwrap.js | 48 ++---
tests/view/writer/wrap.js | 50 ++---
tests/view/writer/wrapposition.js | 274 ++++++++++++++-------------
13 files changed, 396 insertions(+), 338 deletions(-)
diff --git a/src/view/writer.js b/src/view/writer.js
index e002b39b6..10beef718 100644
--- a/src/view/writer.js
+++ b/src/view/writer.js
@@ -732,7 +732,7 @@ export default class Writer {
// @param {Number} startOffset
// @param {Number} endOffset
// @param {module:engine/view/element~Element} attribute
- wrapChildren( parent, startOffset, endOffset, attribute ) {
+ _wrapChildren( parent, startOffset, endOffset, attribute ) {
let i = startOffset;
const wrapPositions = [];
@@ -757,7 +757,7 @@ export default class Writer {
}
// If other nested attribute is found start wrapping there.
else if ( isAttribute ) {
- this.wrapChildren( child, 0, child.childCount, attribute );
+ this._wrapChildren( child, 0, child.childCount, attribute );
}
i++;
diff --git a/tests/view/writer/breakattributes.js b/tests/view/writer/breakattributes.js
index f8fe66c14..459aa658c 100644
--- a/tests/view/writer/breakattributes.js
+++ b/tests/view/writer/breakattributes.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { breakAttributes } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import { stringify, parse } from '../../../src/dev-utils/view';
import ContainerElement from '../../../src/view/containerelement';
import AttributeElement from '../../../src/view/attributeelement';
@@ -13,8 +13,14 @@ import Range from '../../../src/view/range';
import Position from '../../../src/view/position';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-describe( 'writer', () => {
- describe( 'breakAttributes', () => {
+describe( 'Writer', () => {
+ describe( 'breakAttributes()', () => {
+ let writer;
+
+ before( () => {
+ writer = new Writer();
+ } );
+
describe( 'break position', () => {
/**
* Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
@@ -26,7 +32,7 @@ describe( 'writer', () => {
function test( input, expected ) {
const { view, selection } = parse( input );
- const newPosition = breakAttributes( selection.getFirstPosition() );
+ const newPosition = writer.breakAttributes( selection.getFirstPosition() );
expect( stringify( view.root, newPosition, {
showType: true,
showPriority: true
@@ -138,7 +144,7 @@ describe( 'writer', () => {
function test( input, expected ) {
const { view, selection } = parse( input );
- const newRange = breakAttributes( selection.getFirstRange() );
+ const newRange = writer.breakAttributes( selection.getFirstRange() );
expect( stringify( view.root, newRange, { showType: true } ) ).to.equal( expected );
}
@@ -147,7 +153,7 @@ describe( 'writer', () => {
const p2 = new ContainerElement( 'p' );
expect( () => {
- breakAttributes( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
+ writer.breakAttributes( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -155,7 +161,7 @@ describe( 'writer', () => {
const el = new AttributeElement( 'b' );
expect( () => {
- breakAttributes( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
+ writer.breakAttributes( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -237,7 +243,7 @@ describe( 'writer', () => {
const position = new Position( img, 0 );
expect( () => {
- breakAttributes( position );
+ writer.breakAttributes( position );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
} );
@@ -248,7 +254,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( img, 0, b, 0 );
expect( () => {
- breakAttributes( range );
+ writer.breakAttributes( range );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
} );
@@ -258,7 +264,7 @@ describe( 'writer', () => {
const position = new Position( span, 0 );
expect( () => {
- breakAttributes( position );
+ writer.breakAttributes( position );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
} );
@@ -269,7 +275,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( span, 0, b, 0 );
expect( () => {
- breakAttributes( range );
+ writer.breakAttributes( range );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
} );
} );
diff --git a/tests/view/writer/breakcontainer.js b/tests/view/writer/breakcontainer.js
index ef6b45708..22d959930 100644
--- a/tests/view/writer/breakcontainer.js
+++ b/tests/view/writer/breakcontainer.js
@@ -3,28 +3,32 @@
* For licensing, see LICENSE.md.
*/
-import { breakContainer } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import { stringify, parse } from '../../../src/dev-utils/view';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import ContainerElement from '../../../src/view/containerelement';
import Position from '../../../src/view/position';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
- * test break position.
- *
- * @param {String} input
- * @param {String} expected
- */
- function test( input, expected ) {
- const { view, selection } = parse( input );
+describe( 'Writer', () => {
+ describe( 'breakContainer()', () => {
+ let writer;
- const newPosition = breakContainer( selection.getFirstPosition() );
- expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
- }
+ // Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+ // test break position.
+ //
+ // @param {String} input
+ // @param {String} expected
+ function test( input, expected ) {
+ const { view, selection } = parse( input );
+
+ const newPosition = writer.breakContainer( selection.getFirstPosition() );
+ expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
- describe( 'breakContainer', () => {
it( 'break inside element - should break container element at given position', () => {
test(
'' +
@@ -62,7 +66,7 @@ describe( 'writer', () => {
const { selection } = parse( 'foo{}bar' );
expect( () => {
- breakContainer( selection.getFirstPosition() );
+ writer.breakContainer( selection.getFirstPosition() );
} ).to.throw( CKEditorError, /view-writer-break-non-container-element/ );
} );
@@ -71,7 +75,7 @@ describe( 'writer', () => {
const position = Position.createAt( element, 0 );
expect( () => {
- breakContainer( position );
+ writer.breakContainer( position );
} ).to.throw( CKEditorError, /view-writer-break-root/ );
} );
} );
diff --git a/tests/view/writer/clear.js b/tests/view/writer/clear.js
index 065f3e368..5947ee9e3 100644
--- a/tests/view/writer/clear.js
+++ b/tests/view/writer/clear.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { clear } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import Range from '../../../src/view/range';
import { stringify, parse } from '../../../src/dev-utils/view';
import ContainerElement from '../../../src/view/containerelement';
@@ -12,27 +12,33 @@ import EmptyElement from '../../../src/view/emptyelement';
import UIElement from '../../../src/view/uielement';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-describe( 'writer', () => {
- // Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
- //
- // @param {Object} elementToRemove
- // @param {String} input
- // @param {String} expectedResult
- function test( elementToRemove, input, expectedResult ) {
- const { view, selection } = parse( input );
+describe( 'Writer', () => {
+ describe( 'clear()', () => {
+ let writer;
- clear( selection.getFirstRange(), elementToRemove );
+ // Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
+ //
+ // @param {Object} elementToRemove
+ // @param {String} input
+ // @param {String} expectedResult
+ function test( elementToRemove, input, expectedResult ) {
+ const { view, selection } = parse( input );
- expect( stringify( view, null, { showType: true } ) ).to.equal( expectedResult );
- }
+ writer.clear( selection.getFirstRange(), elementToRemove );
+
+ expect( stringify( view, null, { showType: true } ) ).to.equal( expectedResult );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
- describe( 'clear', () => {
it( 'should throw when range placed in two containers', () => {
const p1 = new ContainerElement( 'p' );
const p2 = new ContainerElement( 'p' );
expect( () => {
- clear( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
+ writer.clear( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -40,7 +46,7 @@ describe( 'writer', () => {
const el = new AttributeElement( 'b' );
expect( () => {
- clear( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
+ writer.clear( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
diff --git a/tests/view/writer/insert.js b/tests/view/writer/insert.js
index e0fdb45e2..e31d92295 100644
--- a/tests/view/writer/insert.js
+++ b/tests/view/writer/insert.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { insert } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import ContainerElement from '../../../src/view/containerelement';
import Element from '../../../src/view/element';
import EmptyElement from '../../../src/view/emptyelement';
@@ -13,23 +13,27 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import { stringify, parse } from '../../../src/dev-utils/view';
import AttributeElement from '../../../src/view/attributeelement';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions.
- *
- * @param {String} input
- * @param {Array.} nodesToInsert
- * @param {String} expected
- */
- function test( input, nodesToInsert, expected ) {
- nodesToInsert = nodesToInsert.map( node => parse( node ) );
- const { view, selection } = parse( input );
-
- const newRange = insert( selection.getFirstPosition(), nodesToInsert );
- expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
- }
-
- describe( 'insert', () => {
+describe( 'Writer', () => {
+ describe( 'insert()', () => {
+ let writer;
+
+ // Executes test using `parse` and `stringify` utils functions.
+ //
+ // @param {String} input
+ // @param {Array.} nodesToInsert
+ // @param {String} expected
+ function test( input, nodesToInsert, expected ) {
+ nodesToInsert = nodesToInsert.map( node => parse( node ) );
+ const { view, selection } = parse( input );
+
+ const newRange = writer.insert( selection.getFirstPosition(), nodesToInsert );
+ expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
it( 'should return collapsed range in insertion position when using empty array', () => {
test(
'foo{}bar',
@@ -151,7 +155,7 @@ describe( 'writer', () => {
const container = new ContainerElement( 'p' );
const position = new Position( container, 0 );
expect( () => {
- insert( position, element );
+ writer.insert( position, element );
} ).to.throw( CKEditorError, 'view-writer-insert-invalid-node' );
} );
@@ -162,7 +166,7 @@ describe( 'writer', () => {
const position = new Position( container, 0 );
expect( () => {
- insert( position, root );
+ writer.insert( position, root );
} ).to.throw( CKEditorError, 'view-writer-insert-invalid-node' );
} );
@@ -172,7 +176,7 @@ describe( 'writer', () => {
const attributeElement = new AttributeElement( 'i' );
expect( () => {
- insert( position, attributeElement );
+ writer.insert( position, attributeElement );
} ).to.throw( CKEditorError, 'view-writer-invalid-position-container' );
} );
@@ -191,7 +195,7 @@ describe( 'writer', () => {
const attributeElement = new AttributeElement( 'i' );
expect( () => {
- insert( position, attributeElement );
+ writer.insert( position, attributeElement );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
} );
@@ -202,7 +206,7 @@ describe( 'writer', () => {
const attributeElement = new AttributeElement( 'i' );
expect( () => {
- insert( position, attributeElement );
+ writer.insert( position, attributeElement );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
} );
} );
diff --git a/tests/view/writer/mergeattributes.js b/tests/view/writer/mergeattributes.js
index caa908fe5..eafe629f3 100644
--- a/tests/view/writer/mergeattributes.js
+++ b/tests/view/writer/mergeattributes.js
@@ -3,27 +3,31 @@
* For licensing, see LICENSE.md.
*/
-import { mergeAttributes } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import ContainerElement from '../../../src/view/containerelement';
import Text from '../../../src/view/text';
import Position from '../../../src/view/position';
import { stringify, parse } from '../../../src/dev-utils/view';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
- * test merge position.
- *
- * @param {String} input
- * @param {String} expected
- */
- function test( input, expected ) {
- const { view, selection } = parse( input );
- const newPosition = mergeAttributes( selection.getFirstPosition() );
- expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
- }
-
+describe( 'Writer', () => {
describe( 'mergeAttributes', () => {
+ let writer;
+
+ // Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+ // test merge position.
+ //
+ // @param {String} input
+ // @param {String} expected
+ function test( input, expected ) {
+ const { view, selection } = parse( input );
+ const newPosition = writer.mergeAttributes( selection.getFirstPosition() );
+ expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
it( 'should not merge if inside text node', () => {
test( 'fo{}bar', 'fo{}bar' );
} );
@@ -63,7 +67,7 @@ describe( 'writer', () => {
const p = new ContainerElement( 'p', null, [ t1, t2 ] );
const position = new Position( p, 1 );
- const newPosition = mergeAttributes( position );
+ const newPosition = writer.mergeAttributes( position );
expect( stringify( p, newPosition ) ).to.equal( '
foo{}bar
' );
} );
diff --git a/tests/view/writer/mergecontainers.js b/tests/view/writer/mergecontainers.js
index a1c7360a7..c4a074871 100644
--- a/tests/view/writer/mergecontainers.js
+++ b/tests/view/writer/mergecontainers.js
@@ -3,26 +3,30 @@
* For licensing, see LICENSE.md.
*/
-import { mergeContainers } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import { stringify, parse } from '../../../src/dev-utils/view';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
- * test break position.
- *
- * @param {String} input
- * @param {String} expected
- */
- function test( input, expected ) {
- const { view, selection } = parse( input );
-
- const newPosition = mergeContainers( selection.getFirstPosition() );
- expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
- }
-
- describe( 'mergeContainers', () => {
+describe( 'Writer', () => {
+ describe( 'mergeContainers()', () => {
+ let writer;
+
+ // Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+ // test break position.
+ //
+ // @param {String} input
+ // @param {String} expected
+ function test( input, expected ) {
+ const { view, selection } = parse( input );
+
+ const newPosition = writer.mergeContainers( selection.getFirstPosition() );
+ expect( stringify( view.root, newPosition, { showType: true, showPriority: false } ) ).to.equal( expected );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
it( 'should merge two container elements - position between elements', () => {
test(
'' +
@@ -54,7 +58,7 @@ describe( 'writer', () => {
const { selection } = parse( '[]foobar' );
expect( () => {
- mergeContainers( selection.getFirstPosition() );
+ writer.mergeContainers( selection.getFirstPosition() );
} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
} );
@@ -62,7 +66,7 @@ describe( 'writer', () => {
const { selection } = parse( 'foobar[]' );
expect( () => {
- mergeContainers( selection.getFirstPosition() );
+ writer.mergeContainers( selection.getFirstPosition() );
} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
} );
@@ -70,7 +74,7 @@ describe( 'writer', () => {
const { selection } = parse( 'foo[]bar' );
expect( () => {
- mergeContainers( selection.getFirstPosition() );
+ writer.mergeContainers( selection.getFirstPosition() );
} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
} );
@@ -78,7 +82,7 @@ describe( 'writer', () => {
const { selection } = parse( 'foo[]bar' );
expect( () => {
- mergeContainers( selection.getFirstPosition() );
+ writer.mergeContainers( selection.getFirstPosition() );
} ).to.throw( CKEditorError, /view-writer-merge-containers-invalid-position/ );
} );
} );
diff --git a/tests/view/writer/move.js b/tests/view/writer/move.js
index 48bb19fe1..9b4320168 100644
--- a/tests/view/writer/move.js
+++ b/tests/view/writer/move.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { move } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import { stringify, parse } from '../../../src/dev-utils/view';
import ContainerElement from '../../../src/view/containerelement';
import AttributeElement from '../../../src/view/attributeelement';
@@ -13,26 +13,30 @@ import Range from '../../../src/view/range';
import Position from '../../../src/view/position';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
- * test ranges.
- *
- * @param {String} input
- * @param {String} expectedResult
- * @param {String} expectedRemoved
- */
- function test( source, destination, sourceAfterMove, destinationAfterMove ) {
- const { view: srcView, selection: srcSelection } = parse( source );
- const { view: dstView, selection: dstSelection } = parse( destination );
-
- const newRange = move( srcSelection.getFirstRange(), dstSelection.getFirstPosition() );
-
- expect( stringify( dstView, newRange, { showType: true, showPriority: true } ) ).to.equal( destinationAfterMove );
- expect( stringify( srcView, null, { showType: true, showPriority: true } ) ).to.equal( sourceAfterMove );
- }
-
- describe( 'move', () => {
+describe( 'Writer', () => {
+ describe( 'move()', () => {
+ let writer;
+
+ // Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+ // test ranges.
+ //
+ // @param {String} input
+ // @param {String} expectedResult
+ // @param {String} expectedRemoved
+ function test( source, destination, sourceAfterMove, destinationAfterMove ) {
+ const { view: srcView, selection: srcSelection } = parse( source );
+ const { view: dstView, selection: dstSelection } = parse( destination );
+
+ const newRange = writer.move( srcSelection.getFirstRange(), dstSelection.getFirstPosition() );
+
+ expect( stringify( dstView, newRange, { showType: true, showPriority: true } ) ).to.equal( destinationAfterMove );
+ expect( stringify( srcView, null, { showType: true, showPriority: true } ) ).to.equal( sourceAfterMove );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
it( 'should move single text node', () => {
test(
'[foobar]',
@@ -111,7 +115,7 @@ describe( 'writer', () => {
it( 'should correctly move text nodes inside same parent', () => {
const { view, selection } = parse( '[a]bc' );
- const newRange = move( selection.getFirstRange(), Position.createAt( view, 2 ) );
+ const newRange = writer.move( selection.getFirstRange(), Position.createAt( view, 2 ) );
const expectedView = 'b[a}c';
expect( stringify( view, newRange, { showType: true } ) ).to.equal( expectedView );
@@ -123,7 +127,7 @@ describe( 'writer', () => {
);
const viewText = view.getChild( 3 );
- const newRange = move( selection.getFirstRange(), Position.createAt( viewText, 1 ) );
+ const newRange = writer.move( selection.getFirstRange(), Position.createAt( viewText, 1 ) );
expect( stringify( view, newRange, { showType: true } ) ).to.equal(
'ady[bxxc]y'
@@ -149,7 +153,7 @@ describe( 'writer', () => {
const dstPosition = new Position( dstEmpty, 0 );
expect( () => {
- move( srcRange, dstPosition );
+ writer.move( srcRange, dstPosition );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
} );
@@ -172,7 +176,7 @@ describe( 'writer', () => {
const dstPosition = new Position( dstUI, 0 );
expect( () => {
- move( srcRange, dstPosition );
+ writer.move( srcRange, dstPosition );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
} );
} );
diff --git a/tests/view/writer/remove.js b/tests/view/writer/remove.js
index ecced8a9e..60be0a673 100644
--- a/tests/view/writer/remove.js
+++ b/tests/view/writer/remove.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { remove } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import ContainerElement from '../../../src/view/containerelement';
import Range from '../../../src/view/range';
import DocumentFragment from '../../../src/view/documentfragment';
@@ -13,31 +13,35 @@ import EmptyElement from '../../../src/view/emptyelement';
import UIElement from '../../../src/view/uielement';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
- * test ranges.
- *
- * @param {String} input
- * @param {String} expectedResult
- * @param {String} expectedRemoved
- */
- function test( input, expectedResult, expectedRemoved ) {
- const { view, selection } = parse( input );
-
- const range = selection.getFirstRange();
- const removed = remove( range );
- expect( stringify( view, range, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
- expect( stringify( removed, null, { showType: true, showPriority: true } ) ).to.equal( expectedRemoved );
- }
-
- describe( 'remove', () => {
+describe( 'Writer', () => {
+ describe( 'remove()', () => {
+ let writer;
+
+ // Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+ // test ranges.
+ //
+ // @param {String} input
+ // @param {String} expectedResult
+ // @param {String} expectedRemoved
+ function test( input, expectedResult, expectedRemoved ) {
+ const { view, selection } = parse( input );
+
+ const range = selection.getFirstRange();
+ const removed = writer.remove( range );
+ expect( stringify( view, range, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
+ expect( stringify( removed, null, { showType: true, showPriority: true } ) ).to.equal( expectedRemoved );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
it( 'should throw when range placed in two containers', () => {
const p1 = new ContainerElement( 'p' );
const p2 = new ContainerElement( 'p' );
expect( () => {
- remove( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
+ writer.remove( Range.createFromParentsAndOffsets( p1, 0, p2, 0 ) );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -45,14 +49,14 @@ describe( 'writer', () => {
const el = new AttributeElement( 'b' );
expect( () => {
- remove( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
+ writer.remove( Range.createFromParentsAndOffsets( el, 0, el, 0 ) );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
it( 'should return empty DocumentFragment when range is collapsed', () => {
const p = new ContainerElement( 'p' );
const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
- const fragment = remove( range );
+ const fragment = writer.remove( range );
expect( fragment ).to.be.instanceof( DocumentFragment );
expect( fragment.childCount ).to.equal( 0 );
@@ -126,7 +130,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( emptyElement, 0, attributeElement, 0 );
expect( () => {
- remove( range );
+ writer.remove( range );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
} );
@@ -145,7 +149,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( uiElement, 0, attributeElement, 0 );
expect( () => {
- remove( range );
+ writer.remove( range );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
} );
} );
diff --git a/tests/view/writer/rename.js b/tests/view/writer/rename.js
index a7020ade3..a020a0051 100644
--- a/tests/view/writer/rename.js
+++ b/tests/view/writer/rename.js
@@ -3,12 +3,16 @@
* For licensing, see LICENSE.md.
*/
-import { rename } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import { parse } from '../../../src/dev-utils/view';
-describe( 'writer', () => {
- describe( 'rename', () => {
- let root, foo;
+describe( 'Writer', () => {
+ describe( 'rename()', () => {
+ let root, foo, writer;
+
+ before( () => {
+ writer = new Writer();
+ } );
beforeEach( () => {
root = parse( 'xxx' );
@@ -19,7 +23,7 @@ describe( 'writer', () => {
it( 'should rename given element by inserting a new element in the place of the old one', () => {
const text = foo.getChild( 0 );
- rename( foo, 'bar' );
+ writer.rename( foo, 'bar' );
const bar = root.getChild( 0 );
@@ -30,7 +34,7 @@ describe( 'writer', () => {
} );
it( 'should return a reference to the inserted element', () => {
- const bar = rename( foo, 'bar' );
+ const bar = writer.rename( foo, 'bar' );
expect( bar ).to.equal( root.getChild( 0 ) );
} );
diff --git a/tests/view/writer/unwrap.js b/tests/view/writer/unwrap.js
index 281c13945..288ab1776 100644
--- a/tests/view/writer/unwrap.js
+++ b/tests/view/writer/unwrap.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { unwrap } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import Element from '../../../src/view/element';
import ContainerElement from '../../../src/view/containerelement';
import AttributeElement from '../../../src/view/attributeelement';
@@ -15,22 +15,26 @@ import Text from '../../../src/view/text';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import { stringify, parse } from '../../../src/dev-utils/view';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions.
- *
- * @param {String} input
- * @param {String} unwrapAttribute
- * @param {String} expected
- */
- function test( input, unwrapAttribute, expected ) {
- const { view, selection } = parse( input );
-
- const newRange = unwrap( selection.getFirstRange(), parse( unwrapAttribute ) );
- expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
- }
-
- describe( 'unwrap', () => {
+describe( 'Writer', () => {
+ describe( 'unwrap()', () => {
+ let writer;
+
+ // Executes test using `parse` and `stringify` utils functions.
+ //
+ // @param {String} input
+ // @param {String} unwrapAttribute
+ // @param {String} expected
+ function test( input, unwrapAttribute, expected ) {
+ const { view, selection } = parse( input );
+
+ const newRange = writer.unwrap( selection.getFirstRange(), parse( unwrapAttribute ) );
+ expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
it( 'should do nothing on collapsed ranges', () => {
test(
'f{}oo',
@@ -56,7 +60,7 @@ describe( 'writer', () => {
const b = new Element( 'b' );
expect( () => {
- unwrap( range, b );
+ writer.unwrap( range, b );
} ).to.throw( CKEditorError, 'view-writer-unwrap-invalid-attribute' );
} );
@@ -70,7 +74,7 @@ describe( 'writer', () => {
const b = new AttributeElement( 'b' );
expect( () => {
- unwrap( range, b );
+ writer.unwrap( range, b );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -79,7 +83,7 @@ describe( 'writer', () => {
const b = new AttributeElement( 'b' );
expect( () => {
- unwrap( Range.createFromParentsAndOffsets( el, 0, el, 0 ), b );
+ writer.unwrap( Range.createFromParentsAndOffsets( el, 0, el, 0 ), b );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -345,7 +349,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( empty, 0, container, 2 );
expect( () => {
- unwrap( range, attribute );
+ writer.unwrap( range, attribute );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
} );
@@ -364,7 +368,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( uiElement, 0, container, 2 );
expect( () => {
- unwrap( range, attribute );
+ writer.unwrap( range, attribute );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
} );
} );
diff --git a/tests/view/writer/wrap.js b/tests/view/writer/wrap.js
index b538a9e9e..bebec26d0 100644
--- a/tests/view/writer/wrap.js
+++ b/tests/view/writer/wrap.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { wrap } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import Element from '../../../src/view/element';
import ContainerElement from '../../../src/view/containerelement';
import AttributeElement from '../../../src/view/attributeelement';
@@ -15,22 +15,28 @@ import Text from '../../../src/view/text';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import { stringify, parse } from '../../../src/dev-utils/view';
-describe( 'writer', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions.
- *
- * @param {String} input
- * @param {String} wrapAttribute
- * @param {String} expected
- */
- function test( input, wrapAttribute, expected ) {
- const { view, selection } = parse( input );
- const newRange = wrap( selection.getFirstRange(), parse( wrapAttribute ) );
-
- expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
- }
-
- describe( 'wrap', () => {
+describe( 'Writer', () => {
+ describe( 'wrap()', () => {
+ let writer;
+
+ /**
+ * Executes test using `parse` and `stringify` utils functions.
+ *
+ * @param {String} input
+ * @param {String} wrapAttribute
+ * @param {String} expected
+ */
+ function test( input, wrapAttribute, expected ) {
+ const { view, selection } = parse( input );
+ const newRange = writer.wrap( selection.getFirstRange(), parse( wrapAttribute ) );
+
+ expect( stringify( view.root, newRange, { showType: true, showPriority: true } ) ).to.equal( expected );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
it( 'should do nothing on collapsed ranges', () => {
test(
'f{}oo',
@@ -64,7 +70,7 @@ describe( 'writer', () => {
const b = new Element( 'b' );
expect( () => {
- wrap( range, b );
+ writer.wrap( range, b );
} ).to.throw( CKEditorError, 'view-writer-wrap-invalid-attribute' );
} );
@@ -78,7 +84,7 @@ describe( 'writer', () => {
const b = new AttributeElement( 'b' );
expect( () => {
- wrap( range, b );
+ writer.wrap( range, b );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -87,7 +93,7 @@ describe( 'writer', () => {
const b = new AttributeElement( 'b' );
expect( () => {
- wrap( Range.createFromParentsAndOffsets( el, 0, el, 0 ), b );
+ writer.wrap( Range.createFromParentsAndOffsets( el, 0, el, 0 ), b );
} ).to.throw( CKEditorError, 'view-writer-invalid-range-container' );
} );
@@ -324,7 +330,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( emptyElement, 0, container, 1 );
expect( () => {
- wrap( range, new AttributeElement( 'b' ) );
+ writer.wrap( range, new AttributeElement( 'b' ) );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-empty-element' );
} );
@@ -342,7 +348,7 @@ describe( 'writer', () => {
const range = Range.createFromParentsAndOffsets( uiElement, 0, container, 1 );
expect( () => {
- wrap( range, new AttributeElement( 'b' ) );
+ writer.wrap( range, new AttributeElement( 'b' ) );
} ).to.throw( CKEditorError, 'view-writer-cannot-break-ui-element' );
} );
diff --git a/tests/view/writer/wrapposition.js b/tests/view/writer/wrapposition.js
index 3e23f0cea..f769895d7 100644
--- a/tests/view/writer/wrapposition.js
+++ b/tests/view/writer/wrapposition.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/
-import { wrapPosition } from '../../../src/view/writer';
+import Writer from '../../../src/view/writer';
import Text from '../../../src/view/text';
import Element from '../../../src/view/element';
import ContainerElement from '../../../src/view/containerelement';
@@ -14,137 +14,145 @@ import Position from '../../../src/view/position';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import { stringify, parse } from '../../../src/dev-utils/view';
-describe( 'wrapPosition', () => {
- /**
- * Executes test using `parse` and `stringify` utils functions.
- *
- * @param {String} input
- * @param {String} unwrapAttribute
- * @param {String} expected
- */
- function test( input, unwrapAttribute, expected ) {
- const { view, selection } = parse( input );
-
- const newPosition = wrapPosition( selection.getFirstPosition(), parse( unwrapAttribute ) );
- expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
- }
-
- it( 'should throw error when element is not instance of AttributeElement', () => {
- const container = new ContainerElement( 'p', null, new Text( 'foo' ) );
- const position = new Position( container, 0 );
- const b = new Element( 'b' );
-
- expect( () => {
- wrapPosition( position, b );
- } ).to.throw( CKEditorError, 'view-writer-wrap-invalid-attribute' );
- } );
-
- it( 'should wrap position at the beginning of text node', () => {
- test(
- '{}foobar',
- '',
- '[]foobar'
- );
- } );
-
- it( 'should wrap position inside text node', () => {
- test(
- 'foo{}bar',
- '',
- 'foo[]bar'
- );
- } );
-
- it( 'should support unicode', () => {
- test(
- 'நிலை{}க்கு',
- '',
- 'நிலை[]க்கு'
- );
- } );
-
- it( 'should wrap position inside document fragment', () => {
- test(
- 'foo[]bar',
- '',
- 'foo[]' +
- 'bar'
- );
- } );
-
- it( 'should wrap position at the end of text node', () => {
- test(
- 'foobar{}',
- '',
- 'foobar[]'
- );
- } );
-
- it( 'should merge with existing attributes #1', () => {
- test(
- 'foo[]',
- '',
- 'foo{}'
- );
- } );
-
- it( 'should merge with existing attributes #2', () => {
- test(
- '[]foo',
- '',
- '{}foo'
- );
- } );
-
- it( 'should wrap when inside nested attributes', () => {
- test(
- 'foo{}bar',
- '',
- '' +
- '' +
- 'foo' +
- '[]' +
- 'bar' +
- '' +
- ''
- );
- } );
-
- it( 'should merge when wrapping between same attribute', () => {
- test(
- 'foo[]bar',
- '',
- 'foo{}bar'
- );
- } );
-
- it( 'should move position to text node if in same attribute', () => {
- test(
- 'foobar[]',
- '',
- 'foobar{}'
- );
- } );
-
- it( 'should throw if position is set inside EmptyElement', () => {
- const emptyElement = new EmptyElement( 'img' );
- new ContainerElement( 'p', null, emptyElement ); // eslint-disable-line no-new
- const attributeElement = new AttributeElement( 'b' );
- const position = new Position( emptyElement, 0 );
-
- expect( () => {
- wrapPosition( position, attributeElement );
- } ).to.throw( CKEditorError, 'view-emptyelement-cannot-add' );
- } );
-
- it( 'should throw if position is set inside UIElement', () => {
- const uiElement = new UIElement( 'span' );
- new ContainerElement( 'p', null, uiElement ); // eslint-disable-line no-new
- const attributeElement = new AttributeElement( 'b' );
- const position = new Position( uiElement, 0 );
-
- expect( () => {
- wrapPosition( position, attributeElement );
- } ).to.throw( CKEditorError, 'view-uielement-cannot-add' );
+describe( 'Writer', () => {
+ describe( 'wrapPosition()', () => {
+ let writer;
+
+ // Executes test using `parse` and `stringify` utils functions.
+ //
+ // @param {String} input
+ // @param {String} unwrapAttribute
+ // @param {String} expected
+ function test( input, unwrapAttribute, expected ) {
+ const { view, selection } = parse( input );
+
+ const newPosition = writer.wrapPosition( selection.getFirstPosition(), parse( unwrapAttribute ) );
+ expect( stringify( view, newPosition, { showType: true, showPriority: true } ) ).to.equal( expected );
+ }
+
+ before( () => {
+ writer = new Writer();
+ } );
+
+ it( 'should throw error when element is not instance of AttributeElement', () => {
+ const container = new ContainerElement( 'p', null, new Text( 'foo' ) );
+ const position = new Position( container, 0 );
+ const b = new Element( 'b' );
+
+ expect( () => {
+ writer.wrapPosition( position, b );
+ } ).to.throw( CKEditorError, 'view-writer-wrap-invalid-attribute' );
+ } );
+
+ it( 'should wrap position at the beginning of text node', () => {
+ test(
+ '{}foobar',
+ '',
+ '[]foobar'
+ );
+ } );
+
+ it( 'should wrap position inside text node', () => {
+ test(
+ 'foo{}bar',
+ '',
+ 'foo[]bar'
+ );
+ } );
+
+ it( 'should support unicode', () => {
+ test(
+ 'நிலை{}க்கு',
+ '',
+ 'நிலை[]க்கு'
+ );
+ } );
+
+ it( 'should wrap position inside document fragment', () => {
+ test(
+ 'foo[]bar',
+ '',
+ 'foo[]' +
+ 'bar'
+ );
+ } );
+
+ it( 'should wrap position at the end of text node', () => {
+ test(
+ 'foobar{}',
+ '',
+ 'foobar[]'
+ );
+ } );
+
+ it( 'should merge with existing attributes #1', () => {
+ test(
+ 'foo[]',
+ '',
+ 'foo{}'
+ );
+ } );
+
+ it( 'should merge with existing attributes #2', () => {
+ test(
+ '[]foo',
+ '',
+ '{}foo'
+ );
+ } );
+
+ it( 'should wrap when inside nested attributes', () => {
+ test(
+ 'foo{}bar',
+ '',
+ '' +
+ '' +
+ 'foo' +
+ '[]' +
+ 'bar' +
+ '' +
+ ''
+ );
+ } );
+
+ it( 'should merge when wrapping between same attribute', () => {
+ test(
+ '' +
+ 'foo[]bar' +
+ '',
+ '',
+ 'foo{}bar'
+ );
+ } );
+
+ it( 'should move position to text node if in same attribute', () => {
+ test(
+ 'foobar[]',
+ '',
+ 'foobar{}'
+ );
+ } );
+
+ it( 'should throw if position is set inside EmptyElement', () => {
+ const emptyElement = new EmptyElement( 'img' );
+ new ContainerElement( 'p', null, emptyElement ); // eslint-disable-line no-new
+ const attributeElement = new AttributeElement( 'b' );
+ const position = new Position( emptyElement, 0 );
+
+ expect( () => {
+ writer.wrapPosition( position, attributeElement );
+ } ).to.throw( CKEditorError, 'view-emptyelement-cannot-add' );
+ } );
+
+ it( 'should throw if position is set inside UIElement', () => {
+ const uiElement = new UIElement( 'span' );
+ new ContainerElement( 'p', null, uiElement ); // eslint-disable-line no-new
+ const attributeElement = new AttributeElement( 'b' );
+ const position = new Position( uiElement, 0 );
+
+ expect( () => {
+ writer.wrapPosition( position, attributeElement );
+ } ).to.throw( CKEditorError, 'view-uielement-cannot-add' );
+ } );
} );
} );
From 868d2aaf5e68605c0f63a8cdd7b282bb4fd2e535 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Thu, 11 Jan 2018 16:15:55 +0100
Subject: [PATCH 03/89] Initial view controller skeleton.
---
src/view/view.js | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 src/view/view.js
diff --git a/src/view/view.js b/src/view/view.js
new file mode 100644
index 000000000..19274e7b6
--- /dev/null
+++ b/src/view/view.js
@@ -0,0 +1,41 @@
+import Document from './document';
+import Writer from './writer';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+export default class View {
+ constructor() {
+ this.document = new Document();
+ this._writer = new Writer();
+
+ this._ongoingChange = false;
+ this._renderingInProgress = false;
+ }
+
+ change( callback ) {
+ if ( this._renderingInProgress ) {
+ /**
+ * TODO: description - there might be a view change triggered during rendering process.
+ *
+ * @error applying-view-changes-on-rendering
+ */
+ log.warn(
+ 'applying-view-changes-on-rendering: ' +
+ 'Attempting to make changes in the view during rendering process. ' +
+ 'Your changes will not be rendered in DOM.'
+ );
+ }
+ // If other changes are in progress wait with rendering until every ongoing change is over.
+ if ( this._ongoingChange ) {
+ callback( this._writer );
+ } else {
+ this._ongoingChange = true;
+ callback( this._writer );
+
+ this._renderingInProgress = true;
+ // TODO: render
+ this._renderingInProgress = false;
+
+ this._ongoingChange = false;
+ }
+ }
+}
From 5223afc29493e889b0b3a7c0395364ca065ab1b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Fri, 12 Jan 2018 10:44:13 +0100
Subject: [PATCH 04/89] Moving responsibilites from view document to view
controller.
---
src/controller/editingcontroller.js | 16 ++++----
src/view/document.js | 34 +----------------
src/view/view.js | 58 +++++++++++++++++++++++++++--
3 files changed, 63 insertions(+), 45 deletions(-)
diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js
index d423d0a9c..13dbbba57 100644
--- a/src/controller/editingcontroller.js
+++ b/src/controller/editingcontroller.js
@@ -9,7 +9,7 @@
import RootEditableElement from '../view/rooteditableelement';
import ModelDiffer from '../model/differ';
-import ViewDocument from '../view/document';
+import View from '../view/view';
import Mapper from '../conversion/mapper';
import ModelConversionDispatcher from '../conversion/modelconversiondispatcher';
import {
@@ -50,12 +50,12 @@ export default class EditingController {
this.model = model;
/**
- * View document.
+ * Editing view.
*
* @readonly
- * @member {module:engine/view/document~Document}
+ * @member {module:engine/view/view~View}
*/
- this.view = new ViewDocument();
+ this.view = new View();
/**
* Mapper which describes model-view binding.
@@ -81,7 +81,7 @@ export default class EditingController {
*/
this.modelToView = new ModelConversionDispatcher( this.model, {
mapper: this.mapper,
- viewSelection: this.view.selection
+ viewSelection: this.view.document.selection
} );
// Model differ object. It's role is to buffer changes done on model and then calculates a diff of those changes.
@@ -131,7 +131,7 @@ export default class EditingController {
}, { priority: 'low' } );
// Convert selection from view to model.
- this.listenTo( this.view, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
+ this.listenTo( this.view.document, 'selectionChange', convertSelectionChange( this.model, this.mapper ) );
// Attach default model converters.
this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
@@ -146,7 +146,7 @@ export default class EditingController {
// Binds {@link module:engine/view/document~Document#roots view roots collection} to
// {@link module:engine/model/document~Document#roots model roots collection} so creating
// model root automatically creates corresponding view root.
- this.view.roots.bindTo( this.model.document.roots ).using( root => {
+ this.view.document.roots.bindTo( this.model.document.roots ).using( root => {
// $graveyard is a special root that has no reflection in the view.
if ( root.rootName == '$graveyard' ) {
return null;
@@ -155,7 +155,7 @@ export default class EditingController {
const viewRoot = new RootEditableElement( root.name );
viewRoot.rootName = root.rootName;
- viewRoot.document = this.view;
+ viewRoot.document = this.view.document;
this.mapper.bindElements( root, viewRoot );
return viewRoot;
diff --git a/src/view/document.js b/src/view/document.js
index efd541533..c626d627d 100644
--- a/src/view/document.js
+++ b/src/view/document.js
@@ -23,6 +23,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
+// todo: check the docs
/**
* Document class creates an abstract layer over the content editable area.
* It combines the actual tree of view elements, tree of DOM elements,
@@ -63,16 +64,6 @@ export default class Document {
*/
this.selection = new Selection();
- /**
- * Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by
- * {@link module:engine/view/document~Document#renderer renderer}
- * and {@link module:engine/view/observer/observer~Observer observers}.
- *
- * @readonly
- * @member {module:engine/view/domconverter~DomConverter} module:engine/view/document~Document#domConverter
- */
- this.domConverter = new DomConverter();
-
/**
* Roots of the view tree. Collection of the {module:engine/view/element~Element view elements}.
*
@@ -108,15 +99,6 @@ export default class Document {
*/
this.set( 'isFocused', false );
- /**
- * Instance of the {@link module:engine/view/document~Document#renderer renderer}.
- *
- * @readonly
- * @member {module:engine/view/renderer~Renderer} module:engine/view/document~Document#renderer
- */
- this.renderer = new Renderer( this.domConverter, this.selection );
- this.renderer.bind( 'isFocused' ).to( this );
-
/**
* Map of registered {@link module:engine/view/observer/observer~Observer observers}.
*
@@ -134,8 +116,6 @@ export default class Document {
injectQuirksHandling( this );
injectUiElementHandling( this );
-
- this.decorate( 'render' );
}
/**
@@ -234,18 +214,6 @@ export default class Document {
return this.domRoots.get( name );
}
- /**
- * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
- * before rendering and re-enabled after that.
- *
- * @fires render
- */
- render() {
- this.disableObservers();
- this.renderer.render();
- this.enableObservers();
- }
-
/**
* Focuses document. It will focus {@link module:engine/view/editableelement~EditableElement EditableElement} that is currently having
* selection inside.
diff --git a/src/view/view.js b/src/view/view.js
index 19274e7b6..35b278c66 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -1,12 +1,40 @@
import Document from './document';
import Writer from './writer';
+import Renderer from './renderer';
+import DomConverter from './domconverter';
+
import log from '@ckeditor/ckeditor5-utils/src/log';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import ObservableMixin from '../../../ckeditor5-utils/src/observablemixin';
export default class View {
constructor() {
this.document = new Document();
this._writer = new Writer();
+ // TODO: check docs
+ // TODO: move render event description to this file.
+
+ /**
+ * Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by
+ * {@link module:engine/view/document~Document#renderer renderer}
+ * and {@link module:engine/view/observer/observer~Observer observers}.
+ *
+ * @readonly
+ * @member {module:engine/view/domconverter~DomConverter} module:engine/view/view~View#domConverter
+ */
+ this.domConverter = new DomConverter();
+
+ /**
+ * Instance of the {@link module:engine/view/document~Document#renderer renderer}.
+ *
+ * @readonly
+ * @member {module:engine/view/renderer~Renderer} module:engine/view/view~View#renderer
+ */
+ this._renderer = new Renderer( this.domConverter, this.document.selection );
+ this._renderer.bind( 'isFocused' ).to( this.document );
+ // this.decorate( 'render' );
+
this._ongoingChange = false;
this._renderingInProgress = false;
}
@@ -29,13 +57,35 @@ export default class View {
callback( this._writer );
} else {
this._ongoingChange = true;
- callback( this._writer );
- this._renderingInProgress = true;
- // TODO: render
- this._renderingInProgress = false;
+ callback( this._writer );
+ this._render();
this._ongoingChange = false;
}
}
+
+ /**
+ * Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
+ * before rendering and re-enabled after that.
+ *
+ * @private
+ * @fires render
+ */
+ _render() {
+ this._renderingInProgress = true;
+
+ this.document.disableObservers();
+ this._renderer.render();
+ this.document.enableObservers();
+
+ this._renderingInProgress = false;
+ }
+
+ destroy() {
+ this.document.destroy();
+ this.stopListening();
+ }
}
+
+mix( View, ObservableMixin );
From b53613f127493507483ba2e84a596db6a5ddb1f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Fri, 12 Jan 2018 11:32:30 +0100
Subject: [PATCH 05/89] Moved all DOM related functionality from view document
to view controller.
---
src/view/document.js | 180 -----------------------------------------
src/view/view.js | 186 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 185 insertions(+), 181 deletions(-)
diff --git a/src/view/document.js b/src/view/document.js
index f41efb5be..7997cf514 100644
--- a/src/view/document.js
+++ b/src/view/document.js
@@ -10,16 +10,9 @@
import Selection from './selection';
import { injectQuirksHandling } from './filler';
import { injectUiElementHandling } from './uielement';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-import MutationObserver from './observer/mutationobserver';
-import SelectionObserver from './observer/selectionobserver';
-import FocusObserver from './observer/focusobserver';
-import KeyObserver from './observer/keyobserver';
-import FakeSelectionObserver from './observer/fakeselectionobserver';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
// todo: check the docs
/**
@@ -46,14 +39,6 @@ export default class Document {
* Creates a Document instance.
*/
constructor() {
- /**
- * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
- *
- * @readonly
- * @member {Map} module:engine/view/document~Document#domRoots
- */
- this.domRoots = new Map();
-
/**
* Selection done on this document.
*
@@ -97,99 +82,10 @@ export default class Document {
*/
this.set( 'isFocused', false );
- /**
- * Map of registered {@link module:engine/view/observer/observer~Observer observers}.
- *
- * @private
- * @member {Map.} module:engine/view/document~Document#_observers
- */
- this._observers = new Map();
-
- // Add default observers.
- this.addObserver( MutationObserver );
- this.addObserver( SelectionObserver );
- this.addObserver( FocusObserver );
- this.addObserver( KeyObserver );
- this.addObserver( FakeSelectionObserver );
-
injectQuirksHandling( this );
injectUiElementHandling( this );
}
- /**
- * Creates observer of the given type if not yet created, {@link module:engine/view/observer/observer~Observer#enable enables} it
- * and {@link module:engine/view/observer/observer~Observer#observe attaches} to all existing and future
- * {@link module:engine/view/document~Document#domRoots DOM roots}.
- *
- * Note: Observers are recognized by their constructor (classes). A single observer will be instantiated and used only
- * when registered for the first time. This means that features and other components can register a single observer
- * multiple times without caring whether it has been already added or not.
- *
- * @param {Function} Observer The constructor of an observer to add.
- * Should create an instance inheriting from {@link module:engine/view/observer/observer~Observer}.
- * @returns {module:engine/view/observer/observer~Observer} Added observer instance.
- */
- addObserver( Observer ) {
- let observer = this._observers.get( Observer );
-
- if ( observer ) {
- return observer;
- }
-
- observer = new Observer( this );
-
- this._observers.set( Observer, observer );
-
- for ( const [ name, domElement ] of this.domRoots ) {
- observer.observe( domElement, name );
- }
-
- observer.enable();
-
- return observer;
- }
-
- /**
- * Returns observer of the given type or `undefined` if such observer has not been added yet.
- *
- * @param {Function} Observer The constructor of an observer to get.
- * @returns {module:engine/view/observer/observer~Observer|undefined} Observer instance or undefined.
- */
- getObserver( Observer ) {
- return this._observers.get( Observer );
- }
-
- /**
- * Attaches DOM root element to the view element and enable all observers on that element.
- * Also {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view
- * what means that all child nodes will be removed and replaced with content of the view root.
- *
- * This method also will change view element name as the same as tag name of given dom root.
- * Name is always transformed to lower case.
- *
- * @param {Element} domRoot DOM root element.
- * @param {String} [name='main'] Name of the root.
- */
- attachDomRoot( domRoot, name = 'main' ) {
- const viewRoot = this.getRoot( name );
-
- // Set view root name the same as DOM root tag name.
- viewRoot._name = domRoot.tagName.toLowerCase();
-
- this.domRoots.set( name, domRoot );
- this.domConverter.bindElements( domRoot, viewRoot );
- this.renderer.markToSync( 'children', viewRoot );
- this.renderer.domDocuments.add( domRoot.ownerDocument );
-
- viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
- viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
- viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
-
- for ( const observer of this._observers.values() ) {
- observer.observe( domRoot, name );
- }
- }
-
/**
* Gets a {@link module:engine/view/document~Document#roots view root element} with the specified name. If the name is not
* specific "main" root is returned.
@@ -201,82 +97,6 @@ export default class Document {
getRoot( name = 'main' ) {
return this.roots.get( name );
}
-
- /**
- * Gets DOM root element.
- *
- * @param {String} [name='main'] Name of the root.
- * @returns {Element} DOM root element instance.
- */
- getDomRoot( name = 'main' ) {
- return this.domRoots.get( name );
- }
-
- /**
- * Focuses document. It will focus {@link module:engine/view/editableelement~EditableElement EditableElement} that is currently having
- * selection inside.
- */
- focus() {
- if ( !this.isFocused ) {
- const editable = this.selection.editableElement;
-
- if ( editable ) {
- this.domConverter.focus( editable );
- this.render();
- } else {
- /**
- * Before focusing view document, selection should be placed inside one of the view's editables.
- * Normally its selection will be converted from model document (which have default selection), but
- * when using view document on its own, we need to manually place selection before focusing it.
- *
- * @error view-focus-no-selection
- */
- log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
- }
- }
- }
-
- /**
- * Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
- * caret, if not already visible to the user.
- */
- scrollToTheSelection() {
- const range = this.selection.getFirstRange();
-
- if ( range ) {
- scrollViewportToShowTarget( {
- target: this.domConverter.viewRangeToDom( range ),
- viewportOffset: 20
- } );
- }
- }
-
- /**
- * Disables all added observers.
- */
- disableObservers() {
- for ( const observer of this._observers.values() ) {
- observer.disable();
- }
- }
-
- /**
- * Enables all added observers.
- */
- enableObservers() {
- for ( const observer of this._observers.values() ) {
- observer.enable();
- }
- }
-
- /**
- * Destroys all observers created by view `Document`.
- */
- destroy() {
- for ( const observer of this._observers.values() ) {
- observer.destroy();
- }
- }
}
mix( Document, ObservableMixin );
diff --git a/src/view/view.js b/src/view/view.js
index 35b278c66..b38811491 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -1,11 +1,27 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/document
+ */
+
import Document from './document';
import Writer from './writer';
import Renderer from './renderer';
import DomConverter from './domconverter';
+import MutationObserver from './observer/mutationobserver';
+import KeyObserver from './observer/keyobserver';
+import FakeSelectionObserver from './observer/fakeselectionobserver';
+import SelectionObserver from './observer/selectionobserver';
+import FocusObserver from './observer/focusobserver';
+
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import log from '@ckeditor/ckeditor5-utils/src/log';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
-import ObservableMixin from '../../../ckeditor5-utils/src/observablemixin';
+import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
export default class View {
constructor() {
@@ -35,10 +51,174 @@ export default class View {
this._renderer.bind( 'isFocused' ).to( this.document );
// this.decorate( 'render' );
+ /**
+ * Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
+ *
+ * @readonly
+ * @member {Map} module:engine/view/view~View#domRoots
+ */
+ this.domRoots = new Map();
+
+ /**
+ * Map of registered {@link module:engine/view/observer/observer~Observer observers}.
+ *
+ * @private
+ * @member {Map.} module:engine/view/view~View#_observers
+ */
+ this._observers = new Map();
+
+ // Add default observers.
+ this.addObserver( MutationObserver );
+ this.addObserver( SelectionObserver );
+ this.addObserver( FocusObserver );
+ this.addObserver( KeyObserver );
+ this.addObserver( FakeSelectionObserver );
+
this._ongoingChange = false;
this._renderingInProgress = false;
}
+ /**
+ * Attaches DOM root element to the view element and enable all observers on that element.
+ * Also {@link module:engine/view/renderer~Renderer#markToSync mark element} to be synchronized with the view
+ * what means that all child nodes will be removed and replaced with content of the view root.
+ *
+ * This method also will change view element name as the same as tag name of given dom root.
+ * Name is always transformed to lower case.
+ *
+ * @param {Element} domRoot DOM root element.
+ * @param {String} [name='main'] Name of the root.
+ */
+ attachDomRoot( domRoot, name = 'main' ) {
+ const viewRoot = this.document.getRoot( name );
+
+ // Set view root name the same as DOM root tag name.
+ viewRoot._name = domRoot.tagName.toLowerCase();
+
+ this.domRoots.set( name, domRoot );
+ this.domConverter.bindElements( domRoot, viewRoot );
+ this._renderer.markToSync( 'children', viewRoot );
+ this._renderer.domDocuments.add( domRoot.ownerDocument );
+
+ viewRoot.on( 'change:children', ( evt, node ) => this._renderer.markToSync( 'children', node ) );
+ viewRoot.on( 'change:attributes', ( evt, node ) => this._renderer.markToSync( 'attributes', node ) );
+ viewRoot.on( 'change:text', ( evt, node ) => this._renderer.markToSync( 'text', node ) );
+
+ for ( const observer of this._observers.values() ) {
+ observer.observe( domRoot, name );
+ }
+ }
+
+ /**
+ * Gets DOM root element.
+ *
+ * @param {String} [name='main'] Name of the root.
+ * @returns {Element} DOM root element instance.
+ */
+ getDomRoot( name = 'main' ) {
+ return this.domRoots.get( name );
+ }
+
+ /**
+ * Creates observer of the given type if not yet created, {@link module:engine/view/observer/observer~Observer#enable enables} it
+ * and {@link module:engine/view/observer/observer~Observer#observe attaches} to all existing and future
+ * {@link module:engine/view/document~Document#domRoots DOM roots}.
+ *
+ * Note: Observers are recognized by their constructor (classes). A single observer will be instantiated and used only
+ * when registered for the first time. This means that features and other components can register a single observer
+ * multiple times without caring whether it has been already added or not.
+ *
+ * @param {Function} Observer The constructor of an observer to add.
+ * Should create an instance inheriting from {@link module:engine/view/observer/observer~Observer}.
+ * @returns {module:engine/view/observer/observer~Observer} Added observer instance.
+ */
+ addObserver( Observer ) {
+ let observer = this._observers.get( Observer );
+
+ if ( observer ) {
+ return observer;
+ }
+
+ observer = new Observer( this );
+
+ this._observers.set( Observer, observer );
+
+ for ( const [ name, domElement ] of this.domRoots ) {
+ observer.observe( domElement, name );
+ }
+
+ observer.enable();
+
+ return observer;
+ }
+
+ /**
+ * Returns observer of the given type or `undefined` if such observer has not been added yet.
+ *
+ * @param {Function} Observer The constructor of an observer to get.
+ * @returns {module:engine/view/observer/observer~Observer|undefined} Observer instance or undefined.
+ */
+ getObserver( Observer ) {
+ return this._observers.get( Observer );
+ }
+
+ /**
+ * Disables all added observers.
+ */
+ disableObservers() {
+ for ( const observer of this._observers.values() ) {
+ observer.disable();
+ }
+ }
+
+ /**
+ * Enables all added observers.
+ */
+ enableObservers() {
+ for ( const observer of this._observers.values() ) {
+ observer.enable();
+ }
+ }
+
+ /**
+ * Scrolls the page viewport and {@link #domRoots} with their ancestors to reveal the
+ * caret, if not already visible to the user.
+ */
+ scrollToTheSelection() {
+ const range = this.document.selection.getFirstRange();
+
+ if ( range ) {
+ scrollViewportToShowTarget( {
+ target: this.domConverter.viewRangeToDom( range ),
+ viewportOffset: 20
+ } );
+ }
+ }
+
+ /**
+ * Focuses document. It will focus {@link module:engine/view/editableelement~EditableElement EditableElement} that is currently having
+ * selection inside.
+ */
+ focus() {
+ if ( !this.document.isFocused ) {
+ const editable = this.doocument.selection.editableElement;
+
+ if ( editable ) {
+ this.domConverter.focus( editable );
+ this.render();
+ } else {
+ /**
+ * Before focusing view document, selection should be placed inside one of the view's editables.
+ * Normally its selection will be converted from model document (which have default selection), but
+ * when using view document on its own, we need to manually place selection before focusing it.
+ *
+ * @error view-focus-no-selection
+ */
+ log.warn( 'view-focus-no-selection: There is no selection in any editable to focus.' );
+ }
+ }
+ }
+
change( callback ) {
if ( this._renderingInProgress ) {
/**
@@ -83,6 +263,10 @@ export default class View {
}
destroy() {
+ for ( const observer of this._observers.values() ) {
+ observer.destroy();
+ }
+
this.document.destroy();
this.stopListening();
}
From 73677b6484a016ed6d0b2a701db2ef8a327c77b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Fri, 12 Jan 2018 14:05:31 +0100
Subject: [PATCH 06/89] More things moved from view document to view
controller. Added event.
---
src/view/document.js | 12 ------------
src/view/filler.js | 6 +++---
src/view/uielement.js | 6 +++---
src/view/view.js | 41 ++++++++++++++++++++++++++++-------------
4 files changed, 34 insertions(+), 31 deletions(-)
diff --git a/src/view/document.js b/src/view/document.js
index 7997cf514..f98989440 100644
--- a/src/view/document.js
+++ b/src/view/document.js
@@ -8,8 +8,6 @@
*/
import Selection from './selection';
-import { injectQuirksHandling } from './filler';
-import { injectUiElementHandling } from './uielement';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
@@ -81,9 +79,6 @@ export default class Document {
* @member {Boolean} module:engine/view/document~Document#isFocused
*/
this.set( 'isFocused', false );
-
- injectQuirksHandling( this );
- injectUiElementHandling( this );
}
/**
@@ -112,10 +107,3 @@ mix( Document, ObservableMixin );
*
* @typedef {String} module:engine/view/document~ChangeType
*/
-
-/**
- * Fired when {@link #render render} method is called. Actual rendering is executed as a listener to
- * this event with default priority. This way other listeners can be used to run code before or after rendering.
- *
- * @event render
- */
diff --git a/src/view/filler.js b/src/view/filler.js
index bad6eef44..25b1c55c4 100644
--- a/src/view/filler.js
+++ b/src/view/filler.js
@@ -146,10 +146,10 @@ export function isBlockFiller( domNode, blockFiller ) {
* Assign key observer which move cursor from the end of the inline filler to the beginning of it when
* the left arrow is pressed, so the filler does not break navigation.
*
- * @param {module:engine/view/document~Document} document Document instance we should inject quirks handling on.
+ * @param {module:engine/view/view~View} view View controller instance we should inject quirks handling on.
*/
-export function injectQuirksHandling( document ) {
- document.on( 'keydown', jumpOverInlineFiller );
+export function injectQuirksHandling( view ) {
+ view.document.on( 'keydown', jumpOverInlineFiller );
}
// Move cursor from the end of the inline filler to the beginning of it when, so the filler does not break navigation.
diff --git a/src/view/uielement.js b/src/view/uielement.js
index 13fd45f34..29f99bb5b 100644
--- a/src/view/uielement.js
+++ b/src/view/uielement.js
@@ -90,10 +90,10 @@ export default class UIElement extends Element {
* The callback handles the situation when right arrow key is pressed and selection is collapsed before a UI element.
* Without this handler, it would be impossible to "jump over" UI element using right arrow key.
*
- * @param {module:engine/view/document~Document} document Document to which the quirks handling will be injected.
+ * @param {module:engine/view/view~View} view View controller to which the quirks handling will be injected.
*/
-export function injectUiElementHandling( document ) {
- document.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, document.domConverter ) );
+export function injectUiElementHandling( view ) {
+ view.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, view.domConverter ) );
}
// Returns `null` because block filler is not needed for UIElements.
diff --git a/src/view/view.js b/src/view/view.js
index b38811491..62792451a 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -22,6 +22,8 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import log from '@ckeditor/ckeditor5-utils/src/log';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
+import { injectUiElementHandling } from './uielement';
+import { injectQuirksHandling } from './filler';
export default class View {
constructor() {
@@ -49,7 +51,6 @@ export default class View {
*/
this._renderer = new Renderer( this.domConverter, this.document.selection );
this._renderer.bind( 'isFocused' ).to( this.document );
- // this.decorate( 'render' );
/**
* Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
@@ -74,6 +75,9 @@ export default class View {
this.addObserver( KeyObserver );
this.addObserver( FakeSelectionObserver );
+ injectQuirksHandling( this );
+ injectUiElementHandling( this );
+
this._ongoingChange = false;
this._renderingInProgress = false;
}
@@ -228,10 +232,11 @@ export default class View {
*/
log.warn(
'applying-view-changes-on-rendering: ' +
- 'Attempting to make changes in the view during rendering process. ' +
- 'Your changes will not be rendered in DOM.'
+ 'Attempting to make changes in the view during rendering process.' +
+ 'This may cause some unexpected behaviour and inconsistency between the DOM and the view.'
);
}
+
// If other changes are in progress wait with rendering until every ongoing change is over.
if ( this._ongoingChange ) {
callback( this._writer );
@@ -242,15 +247,34 @@ export default class View {
this._render();
this._ongoingChange = false;
+
+ // TODO: docs for the event.
+ this.fire( 'change' );
+ }
+ }
+
+ render() {
+ // Render only if no ongoing changes in progress. If there are some, view document will be rendered after all
+ // changes are done. This way view document will not be rendered in the middle of some changes.
+ if ( !this._ongoingChange ) {
+ this._render();
}
}
+ destroy() {
+ for ( const observer of this._observers.values() ) {
+ observer.destroy();
+ }
+
+ this.document.destroy();
+ this.stopListening();
+ }
+
/**
* Renders all changes. In order to avoid triggering the observers (e.g. mutations) all observers are disabled
* before rendering and re-enabled after that.
*
* @private
- * @fires render
*/
_render() {
this._renderingInProgress = true;
@@ -261,15 +285,6 @@ export default class View {
this._renderingInProgress = false;
}
-
- destroy() {
- for ( const observer of this._observers.values() ) {
- observer.destroy();
- }
-
- this.document.destroy();
- this.stopListening();
- }
}
mix( View, ObservableMixin );
From a3cb2176c59d3ed1e55d4310544c82a8d663d63e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Fri, 12 Jan 2018 14:37:55 +0100
Subject: [PATCH 07/89] Cleaned tests of view document.
---
src/view/document.js | 12 --
src/view/view.js | 12 ++
tests/view/document/document.js | 355 +-----------------------------
tests/view/view.js | 372 ++++++++++++++++++++++++++++++++
4 files changed, 385 insertions(+), 366 deletions(-)
create mode 100644 tests/view/view.js
diff --git a/src/view/document.js b/src/view/document.js
index f98989440..73be180b1 100644
--- a/src/view/document.js
+++ b/src/view/document.js
@@ -67,18 +67,6 @@ export default class Document {
* @member {Boolean} #isReadOnly
*/
this.set( 'isReadOnly', false );
-
- /**
- * True if document is focused.
- *
- * This property is updated by the {@link module:engine/view/observer/focusobserver~FocusObserver}.
- * If the {@link module:engine/view/observer/focusobserver~FocusObserver} is disabled this property will not change.
- *
- * @readonly
- * @observable
- * @member {Boolean} module:engine/view/document~Document#isFocused
- */
- this.set( 'isFocused', false );
}
/**
diff --git a/src/view/view.js b/src/view/view.js
index 62792451a..4d5f7bd7d 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -68,6 +68,18 @@ export default class View {
*/
this._observers = new Map();
+ /**
+ * True if view is focused.
+ *
+ * This property is updated by the {@link module:engine/view/observer/focusobserver~FocusObserver}.
+ * If the {@link module:engine/view/observer/focusobserver~FocusObserver} is disabled this property will not change.
+ *
+ * @readonly
+ * @observable
+ * @member {Boolean} module:engine/view/document~Document#isFocused
+ */
+ this.set( 'isFocused', false );
+
// Add default observers.
this.addObserver( MutationObserver );
this.addObserver( SelectionObserver );
diff --git a/tests/view/document/document.js b/tests/view/document/document.js
index 4d992f2e2..6c2d7a368 100644
--- a/tests/view/document/document.js
+++ b/tests/view/document/document.js
@@ -7,26 +7,14 @@
import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
import Document from '../../../src/view/document';
-import Observer from '../../../src/view/observer/observer';
-import MutationObserver from '../../../src/view/observer/mutationobserver';
-import SelectionObserver from '../../../src/view/observer/selectionobserver';
-import FocusObserver from '../../../src/view/observer/focusobserver';
-import KeyObserver from '../../../src/view/observer/keyobserver';
-import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
-import Renderer from '../../../src/view/renderer';
-import ViewRange from '../../../src/view/range';
-import DomConverter from '../../../src/view/domconverter';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import count from '@ckeditor/ckeditor5-utils/src/count';
-import log from '@ckeditor/ckeditor5-utils/src/log';
-import global from '@ckeditor/ckeditor5-utils/src/dom/global';
import createViewRoot from '../_utils/createroot';
testUtils.createSinonSandbox();
describe( 'Document', () => {
- const DEFAULT_OBSERVERS_COUNT = 5;
- let ObserverMock, ObserverMockGlobalCount, instantiated, enabled, domRoot, viewDocument;
+ let domRoot, viewDocument;
beforeEach( () => {
domRoot = createElement( document, 'div', {
@@ -35,112 +23,17 @@ describe( 'Document', () => {
} );
document.body.appendChild( domRoot );
- instantiated = 0;
- enabled = 0;
-
- ObserverMock = class extends Observer {
- constructor( viewDocument ) {
- super( viewDocument );
-
- this.enable = sinon.spy();
- this.disable = sinon.spy();
- this.observe = sinon.spy();
- }
- };
-
- ObserverMockGlobalCount = class extends Observer {
- constructor( viewDocument ) {
- super( viewDocument );
- instantiated++;
-
- this.observe = sinon.spy();
- }
-
- enable() {
- enabled++;
- }
- };
-
viewDocument = new Document();
} );
afterEach( () => {
- viewDocument.destroy();
domRoot.remove();
} );
describe( 'constructor()', () => {
it( 'should create Document with all properties', () => {
- expect( count( viewDocument.domRoots ) ).to.equal( 0 );
expect( count( viewDocument.roots ) ).to.equal( 0 );
- expect( viewDocument ).to.have.property( 'renderer' ).to.instanceOf( Renderer );
- expect( viewDocument ).to.have.property( 'domConverter' ).to.instanceOf( DomConverter );
expect( viewDocument ).to.have.property( 'isReadOnly' ).to.false;
- expect( viewDocument ).to.have.property( 'isFocused' ).to.false;
- } );
-
- it( 'should add default observers', () => {
- expect( count( viewDocument._observers ) ).to.equal( DEFAULT_OBSERVERS_COUNT );
- expect( viewDocument.getObserver( MutationObserver ) ).to.be.instanceof( MutationObserver );
- expect( viewDocument.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
- expect( viewDocument.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
- expect( viewDocument.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
- expect( viewDocument.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
- } );
- } );
-
- describe( 'attachDomRoot()', () => {
- it( 'should attach DOM element to main view element', () => {
- const domDiv = document.createElement( 'div' );
- const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
-
- expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-
- viewDocument.attachDomRoot( domDiv );
-
- expect( count( viewDocument.domRoots ) ).to.equal( 1 );
-
- expect( viewDocument.getDomRoot() ).to.equal( domDiv );
- expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
-
- expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
- } );
-
- it( 'should attach DOM element to custom view element', () => {
- const domH1 = document.createElement( 'h1' );
- const viewH1 = createViewRoot( viewDocument, 'h1', 'header' );
-
- expect( count( viewDocument.domRoots ) ).to.equal( 0 );
-
- viewDocument.attachDomRoot( domH1, 'header' );
-
- expect( count( viewDocument.domRoots ) ).to.equal( 1 );
- expect( viewDocument.getDomRoot( 'header' ) ).to.equal( domH1 );
- expect( viewDocument.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
- expect( viewDocument.renderer.markedChildren.has( viewH1 ) ).to.be.true;
- } );
-
- it( 'should call observe on each observer', () => {
- // The variable will be overwritten.
- viewDocument.destroy();
-
- viewDocument = new Document( document.createElement( 'div' ) );
- viewDocument.renderer.render = sinon.spy();
-
- const domDiv1 = document.createElement( 'div' );
- domDiv1.setAttribute( 'id', 'editor' );
-
- const domDiv2 = document.createElement( 'div' );
- domDiv2.setAttribute( 'id', 'editor' );
-
- const observerMock = viewDocument.addObserver( ObserverMock );
- const observerMockGlobalCount = viewDocument.addObserver( ObserverMockGlobalCount );
-
- createViewRoot( viewDocument, 'div', 'root1' );
- viewDocument.attachDomRoot( document.createElement( 'div' ), 'root1' );
-
- sinon.assert.calledOnce( observerMock.observe );
- sinon.assert.calledOnce( observerMockGlobalCount.observe );
} );
} );
@@ -165,250 +58,4 @@ describe( 'Document', () => {
expect( viewDocument.getRoot( 'not-existing' ) ).to.null;
} );
} );
-
- describe( 'addObserver()', () => {
- beforeEach( () => {
- // The variable will be overwritten.
- viewDocument.destroy();
-
- viewDocument = new Document( document.createElement( 'div' ) );
- viewDocument.renderer.render = sinon.spy();
- } );
-
- afterEach( () => {
- viewDocument.destroy();
- } );
-
- it( 'should be instantiated and enabled on adding', () => {
- const observerMock = viewDocument.addObserver( ObserverMock );
-
- expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
-
- expect( observerMock ).to.have.property( 'document', viewDocument );
- sinon.assert.calledOnce( observerMock.enable );
- } );
-
- it( 'should return observer instance each time addObserver is called', () => {
- const observerMock1 = viewDocument.addObserver( ObserverMock );
- const observerMock2 = viewDocument.addObserver( ObserverMock );
-
- expect( observerMock1 ).to.be.instanceof( ObserverMock );
- expect( observerMock2 ).to.be.instanceof( ObserverMock );
- expect( observerMock1 ).to.equals( observerMock2 );
- } );
-
- it( 'should instantiate one observer only once', () => {
- viewDocument.addObserver( ObserverMockGlobalCount );
- viewDocument.addObserver( ObserverMockGlobalCount );
-
- expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
- expect( instantiated ).to.equal( 1 );
- expect( enabled ).to.equal( 1 );
-
- viewDocument.addObserver( ObserverMock );
- expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
- } );
-
- it( 'should instantiate child class of already registered observer', () => {
- class ObserverMock extends Observer {
- enable() {}
- }
- class ChildObserverMock extends ObserverMock {
- enable() {}
- }
-
- viewDocument.addObserver( ObserverMock );
- viewDocument.addObserver( ChildObserverMock );
-
- expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
- } );
-
- it( 'should be disabled and re-enabled on render', () => {
- const observerMock = viewDocument.addObserver( ObserverMock );
- viewDocument.render();
-
- sinon.assert.calledOnce( observerMock.disable );
- sinon.assert.calledOnce( viewDocument.renderer.render );
- sinon.assert.calledTwice( observerMock.enable );
- } );
-
- it( 'should call observe on each root', () => {
- createViewRoot( viewDocument, 'div', 'roo1' );
- createViewRoot( viewDocument, 'div', 'roo2' );
-
- viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo1' );
- viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo2' );
-
- const observerMock = viewDocument.addObserver( ObserverMock );
-
- sinon.assert.calledTwice( observerMock.observe );
- } );
- } );
-
- describe( 'getObserver()', () => {
- it( 'should return observer it it is added', () => {
- const addedObserverMock = viewDocument.addObserver( ObserverMock );
- const getObserverMock = viewDocument.getObserver( ObserverMock );
-
- expect( getObserverMock ).to.be.instanceof( ObserverMock );
- expect( getObserverMock ).to.equal( addedObserverMock );
- } );
-
- it( 'should return undefined if observer is not added', () => {
- const getObserverMock = viewDocument.getObserver( ObserverMock );
-
- expect( getObserverMock ).to.be.undefined;
- } );
- } );
-
- describe( 'scrollToTheSelection()', () => {
- beforeEach( () => {
- // Silence the Rect warnings.
- testUtils.sinon.stub( log, 'warn' );
- } );
-
- it( 'does nothing when there are no ranges in the selection', () => {
- const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
-
- viewDocument.scrollToTheSelection();
- sinon.assert.notCalled( stub );
- } );
-
- it( 'scrolls to the first range in selection with an offset', () => {
- const root = createViewRoot( viewDocument, 'div', 'main' );
- const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
- const range = ViewRange.createIn( root );
-
- viewDocument.attachDomRoot( domRoot );
-
- // Make sure the window will have to scroll to the domRoot.
- Object.assign( domRoot.style, {
- position: 'absolute',
- top: '-1000px',
- left: '-1000px'
- } );
-
- viewDocument.selection.addRange( range );
-
- viewDocument.scrollToTheSelection();
- sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
- } );
- } );
-
- describe( 'disableObservers()', () => {
- it( 'should disable observers', () => {
- const addedObserverMock = viewDocument.addObserver( ObserverMock );
-
- expect( addedObserverMock.enable.calledOnce ).to.be.true;
- expect( addedObserverMock.disable.called ).to.be.false;
-
- viewDocument.disableObservers();
-
- expect( addedObserverMock.enable.calledOnce ).to.be.true;
- expect( addedObserverMock.disable.calledOnce ).to.be.true;
- } );
- } );
-
- describe( 'enableObservers()', () => {
- it( 'should enable observers', () => {
- const addedObserverMock = viewDocument.addObserver( ObserverMock );
-
- viewDocument.disableObservers();
-
- expect( addedObserverMock.enable.calledOnce ).to.be.true;
- expect( addedObserverMock.disable.calledOnce ).to.be.true;
-
- viewDocument.enableObservers();
-
- expect( addedObserverMock.enable.calledTwice ).to.be.true;
- expect( addedObserverMock.disable.calledOnce ).to.be.true;
- } );
- } );
-
- describe( 'isFocused', () => {
- it( 'should change renderer.isFocused too', () => {
- expect( viewDocument.isFocused ).to.equal( false );
- expect( viewDocument.renderer.isFocused ).to.equal( false );
-
- viewDocument.isFocused = true;
-
- expect( viewDocument.isFocused ).to.equal( true );
- expect( viewDocument.renderer.isFocused ).to.equal( true );
- } );
- } );
-
- describe( 'focus()', () => {
- let domEditable, viewEditable;
-
- beforeEach( () => {
- domEditable = document.createElement( 'div' );
- domEditable.setAttribute( 'contenteditable', 'true' );
- document.body.appendChild( domEditable );
- viewEditable = createViewRoot( viewDocument, 'div', 'main' );
- viewDocument.attachDomRoot( domEditable );
- viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
- } );
-
- afterEach( () => {
- document.body.removeChild( domEditable );
- } );
-
- it( 'should focus editable with selection', () => {
- const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
- const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
-
- viewDocument.focus();
-
- expect( converterFocusSpy.called ).to.be.true;
- expect( renderSpy.calledOnce ).to.be.true;
- expect( document.activeElement ).to.equal( domEditable );
- const domSelection = document.getSelection();
- expect( domSelection.rangeCount ).to.equal( 1 );
- const domRange = domSelection.getRangeAt( 0 );
- expect( domRange.startContainer ).to.equal( domEditable );
- expect( domRange.startOffset ).to.equal( 0 );
- expect( domRange.collapsed ).to.be.true;
- } );
-
- it( 'should not focus if document is already focused', () => {
- const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
- const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
- viewDocument.isFocused = true;
-
- viewDocument.focus();
-
- expect( converterFocusSpy.called ).to.be.false;
- expect( renderSpy.called ).to.be.false;
- } );
-
- it( 'should log warning when no selection', () => {
- const logSpy = testUtils.sinon.stub( log, 'warn' );
- viewDocument.selection.removeAllRanges();
-
- viewDocument.focus();
- expect( logSpy.calledOnce ).to.be.true;
- expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^view-focus-no-selection/ );
- } );
- } );
-
- describe( 'render()', () => {
- it( 'should fire an event', () => {
- const spy = sinon.spy();
-
- viewDocument.on( 'render', spy );
-
- viewDocument.render();
-
- expect( spy.calledOnce ).to.be.true;
- } );
-
- it( 'disable observers, renders and enable observers', () => {
- const observerMock = viewDocument.addObserver( ObserverMock );
- const renderStub = sinon.stub( viewDocument.renderer, 'render' );
-
- viewDocument.render();
-
- sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable );
- } );
- } );
} );
diff --git a/tests/view/view.js b/tests/view/view.js
new file mode 100644
index 000000000..cb73d3c0d
--- /dev/null
+++ b/tests/view/view.js
@@ -0,0 +1,372 @@
+/* globals document */
+
+import View from '../../src/view/view';
+import MutationObserver from '../../src/view/observer/mutationobserver';
+import count from '@ckeditor/ckeditor5-utils/src/count';
+import KeyObserver from '../../src/view/observer/keyobserver';
+import FakeSelectionObserver from '../../src/view/observer/fakeselectionobserver';
+import SelectionObserver from '../../src/view/observer/selectionobserver';
+import FocusObserver from '../../src/view/observer/focusobserver';
+import createViewRoot from './_utils/createroot';
+import Document from '../../src/view/document';
+import Observer from '../../src/view/observer/observer';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import ViewRange from '../../src/view/range';
+import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
+
+describe( 'view', () => {
+ const DEFAULT_OBSERVERS_COUNT = 5;
+ let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount;
+
+ beforeEach( () => {
+ domRoot = createElement( document, 'div', {
+ id: 'editor',
+ contenteditable: 'true'
+ } );
+
+ view = new View();
+ viewDocument = view.document;
+
+ ObserverMock = class extends Observer {
+ constructor( viewDocument ) {
+ super( viewDocument );
+
+ this.enable = sinon.spy();
+ this.disable = sinon.spy();
+ this.observe = sinon.spy();
+ }
+ };
+
+ instantiated = 0;
+ enabled = 0;
+
+ ObserverMockGlobalCount = class extends Observer {
+ constructor( viewDocument ) {
+ super( viewDocument );
+ instantiated++;
+
+ this.observe = sinon.spy();
+ }
+
+ enable() {
+ enabled++;
+ }
+ };
+ } );
+
+ afterEach( () => {
+ view.destroy();
+ } );
+
+ it( 'should add default observers', () => {
+ expect( count( view._observers ) ).to.equal( DEFAULT_OBSERVERS_COUNT );
+ expect( view.getObserver( MutationObserver ) ).to.be.instanceof( MutationObserver );
+ expect( view.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
+ expect( view.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
+ expect( view.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
+ expect( view.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
+ } );
+
+ describe( 'attachDomRoot()', () => {
+ it( 'should attach DOM element to main view element', () => {
+ const domDiv = document.createElement( 'div' );
+ const viewRoot = createViewRoot( viewDocument, 'div', 'main' );
+
+ expect( count( viewDocument.domRoots ) ).to.equal( 0 );
+
+ viewDocument.attachDomRoot( domDiv );
+
+ expect( count( viewDocument.domRoots ) ).to.equal( 1 );
+
+ expect( viewDocument.getDomRoot() ).to.equal( domDiv );
+ expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv );
+
+ expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true;
+ } );
+
+ it( 'should attach DOM element to custom view element', () => {
+ const domH1 = document.createElement( 'h1' );
+ const viewH1 = createViewRoot( viewDocument, 'h1', 'header' );
+
+ expect( count( viewDocument.domRoots ) ).to.equal( 0 );
+
+ viewDocument.attachDomRoot( domH1, 'header' );
+
+ expect( count( viewDocument.domRoots ) ).to.equal( 1 );
+ expect( viewDocument.getDomRoot( 'header' ) ).to.equal( domH1 );
+ expect( viewDocument.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 );
+ expect( viewDocument.renderer.markedChildren.has( viewH1 ) ).to.be.true;
+ } );
+
+ it( 'should call observe on each observer', () => {
+ // The variable will be overwritten.
+ viewDocument.destroy();
+
+ viewDocument = new Document( document.createElement( 'div' ) );
+ viewDocument.renderer.render = sinon.spy();
+
+ const domDiv1 = document.createElement( 'div' );
+ domDiv1.setAttribute( 'id', 'editor' );
+
+ const domDiv2 = document.createElement( 'div' );
+ domDiv2.setAttribute( 'id', 'editor' );
+
+ const observerMock = viewDocument.addObserver( ObserverMock );
+ const observerMockGlobalCount = viewDocument.addObserver( ObserverMockGlobalCount );
+
+ createViewRoot( viewDocument, 'div', 'root1' );
+ viewDocument.attachDomRoot( document.createElement( 'div' ), 'root1' );
+
+ sinon.assert.calledOnce( observerMock.observe );
+ sinon.assert.calledOnce( observerMockGlobalCount.observe );
+ } );
+ } );
+
+ describe( 'addObserver()', () => {
+ beforeEach( () => {
+ // The variable will be overwritten.
+ viewDocument.destroy();
+
+ viewDocument = new Document( document.createElement( 'div' ) );
+ viewDocument.renderer.render = sinon.spy();
+ } );
+
+ afterEach( () => {
+ viewDocument.destroy();
+ } );
+
+ it( 'should be instantiated and enabled on adding', () => {
+ const observerMock = viewDocument.addObserver( ObserverMock );
+
+ expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
+
+ expect( observerMock ).to.have.property( 'document', viewDocument );
+ sinon.assert.calledOnce( observerMock.enable );
+ } );
+
+ it( 'should return observer instance each time addObserver is called', () => {
+ const observerMock1 = viewDocument.addObserver( ObserverMock );
+ const observerMock2 = viewDocument.addObserver( ObserverMock );
+
+ expect( observerMock1 ).to.be.instanceof( ObserverMock );
+ expect( observerMock2 ).to.be.instanceof( ObserverMock );
+ expect( observerMock1 ).to.equals( observerMock2 );
+ } );
+
+ it( 'should instantiate one observer only once', () => {
+ viewDocument.addObserver( ObserverMockGlobalCount );
+ viewDocument.addObserver( ObserverMockGlobalCount );
+
+ expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 );
+ expect( instantiated ).to.equal( 1 );
+ expect( enabled ).to.equal( 1 );
+
+ viewDocument.addObserver( ObserverMock );
+ expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
+ } );
+
+ it( 'should instantiate child class of already registered observer', () => {
+ class ObserverMock extends Observer {
+ enable() {}
+ }
+ class ChildObserverMock extends ObserverMock {
+ enable() {}
+ }
+
+ viewDocument.addObserver( ObserverMock );
+ viewDocument.addObserver( ChildObserverMock );
+
+ expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 );
+ } );
+
+ it( 'should be disabled and re-enabled on render', () => {
+ const observerMock = viewDocument.addObserver( ObserverMock );
+ viewDocument.render();
+
+ sinon.assert.calledOnce( observerMock.disable );
+ sinon.assert.calledOnce( viewDocument.renderer.render );
+ sinon.assert.calledTwice( observerMock.enable );
+ } );
+
+ it( 'should call observe on each root', () => {
+ createViewRoot( viewDocument, 'div', 'roo1' );
+ createViewRoot( viewDocument, 'div', 'roo2' );
+
+ viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo1' );
+ viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo2' );
+
+ const observerMock = viewDocument.addObserver( ObserverMock );
+
+ sinon.assert.calledTwice( observerMock.observe );
+ } );
+ } );
+
+ describe( 'getObserver()', () => {
+ it( 'should return observer it it is added', () => {
+ const addedObserverMock = viewDocument.addObserver( ObserverMock );
+ const getObserverMock = viewDocument.getObserver( ObserverMock );
+
+ expect( getObserverMock ).to.be.instanceof( ObserverMock );
+ expect( getObserverMock ).to.equal( addedObserverMock );
+ } );
+
+ it( 'should return undefined if observer is not added', () => {
+ const getObserverMock = viewDocument.getObserver( ObserverMock );
+
+ expect( getObserverMock ).to.be.undefined;
+ } );
+ } );
+
+ describe( 'scrollToTheSelection()', () => {
+ beforeEach( () => {
+ // Silence the Rect warnings.
+ testUtils.sinon.stub( log, 'warn' );
+ } );
+
+ it( 'does nothing when there are no ranges in the selection', () => {
+ const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
+
+ viewDocument.scrollToTheSelection();
+ sinon.assert.notCalled( stub );
+ } );
+
+ it( 'scrolls to the first range in selection with an offset', () => {
+ const root = createViewRoot( viewDocument, 'div', 'main' );
+ const stub = testUtils.sinon.stub( global.window, 'scrollTo' );
+ const range = ViewRange.createIn( root );
+
+ viewDocument.attachDomRoot( domRoot );
+
+ // Make sure the window will have to scroll to the domRoot.
+ Object.assign( domRoot.style, {
+ position: 'absolute',
+ top: '-1000px',
+ left: '-1000px'
+ } );
+
+ viewDocument.selection.addRange( range );
+
+ viewDocument.scrollToTheSelection();
+ sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number );
+ } );
+ } );
+
+ describe( 'disableObservers()', () => {
+ it( 'should disable observers', () => {
+ const addedObserverMock = viewDocument.addObserver( ObserverMock );
+
+ expect( addedObserverMock.enable.calledOnce ).to.be.true;
+ expect( addedObserverMock.disable.called ).to.be.false;
+
+ viewDocument.disableObservers();
+
+ expect( addedObserverMock.enable.calledOnce ).to.be.true;
+ expect( addedObserverMock.disable.calledOnce ).to.be.true;
+ } );
+ } );
+
+ describe( 'enableObservers()', () => {
+ it( 'should enable observers', () => {
+ const addedObserverMock = viewDocument.addObserver( ObserverMock );
+
+ viewDocument.disableObservers();
+
+ expect( addedObserverMock.enable.calledOnce ).to.be.true;
+ expect( addedObserverMock.disable.calledOnce ).to.be.true;
+
+ viewDocument.enableObservers();
+
+ expect( addedObserverMock.enable.calledTwice ).to.be.true;
+ expect( addedObserverMock.disable.calledOnce ).to.be.true;
+ } );
+ } );
+
+ describe( 'focus()', () => {
+ let domEditable, viewEditable;
+
+ beforeEach( () => {
+ domEditable = document.createElement( 'div' );
+ domEditable.setAttribute( 'contenteditable', 'true' );
+ document.body.appendChild( domEditable );
+ viewEditable = createViewRoot( viewDocument, 'div', 'main' );
+ viewDocument.attachDomRoot( domEditable );
+ viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) );
+ } );
+
+ afterEach( () => {
+ document.body.removeChild( domEditable );
+ } );
+
+ it( 'should focus editable with selection', () => {
+ const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
+ const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
+
+ viewDocument.focus();
+
+ expect( converterFocusSpy.called ).to.be.true;
+ expect( renderSpy.calledOnce ).to.be.true;
+ expect( document.activeElement ).to.equal( domEditable );
+ const domSelection = document.getSelection();
+ expect( domSelection.rangeCount ).to.equal( 1 );
+ const domRange = domSelection.getRangeAt( 0 );
+ expect( domRange.startContainer ).to.equal( domEditable );
+ expect( domRange.startOffset ).to.equal( 0 );
+ expect( domRange.collapsed ).to.be.true;
+ } );
+
+ it( 'should not focus if document is already focused', () => {
+ const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' );
+ const renderSpy = testUtils.sinon.spy( viewDocument, 'render' );
+ viewDocument.isFocused = true;
+
+ viewDocument.focus();
+
+ expect( converterFocusSpy.called ).to.be.false;
+ expect( renderSpy.called ).to.be.false;
+ } );
+
+ it( 'should log warning when no selection', () => {
+ const logSpy = testUtils.sinon.stub( log, 'warn' );
+ viewDocument.selection.removeAllRanges();
+
+ viewDocument.focus();
+ expect( logSpy.calledOnce ).to.be.true;
+ expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^view-focus-no-selection/ );
+ } );
+ } );
+
+ describe( 'isFocused', () => {
+ it( 'should change renderer.isFocused too', () => {
+ expect( viewDocument.isFocused ).to.equal( false );
+ expect( viewDocument.renderer.isFocused ).to.equal( false );
+
+ viewDocument.isFocused = true;
+
+ expect( viewDocument.isFocused ).to.equal( true );
+ expect( viewDocument.renderer.isFocused ).to.equal( true );
+ } );
+ } );
+
+ describe( 'render()', () => {
+ it( 'should fire an event', () => {
+ const spy = sinon.spy();
+
+ viewDocument.on( 'render', spy );
+
+ viewDocument.render();
+
+ expect( spy.calledOnce ).to.be.true;
+ } );
+
+ it( 'disable observers, renders and enable observers', () => {
+ const observerMock = viewDocument.addObserver( ObserverMock );
+ const renderStub = sinon.stub( viewDocument.renderer, 'render' );
+
+ viewDocument.render();
+
+ sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable );
+ } );
+ } );
+} );
From 2b365199b250db31408f243ede1f8ca312b19a8d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Mon, 15 Jan 2018 14:58:57 +0100
Subject: [PATCH 08/89] Refactored observers' tests.
---
src/view/document.js | 12 +
src/view/observer/clickobserver.js | 4 +-
src/view/observer/domeventdata.js | 10 +-
src/view/observer/domeventobserver.js | 6 +-
src/view/observer/fakeselectionobserver.js | 6 +-
src/view/observer/focusobserver.js | 9 +-
src/view/observer/keyobserver.js | 4 +-
src/view/observer/mouseobserver.js | 4 +-
src/view/observer/mutationobserver.js | 10 +-
src/view/observer/observer.js | 8 +-
src/view/observer/selectionobserver.js | 20 +-
src/view/renderer.js | 3 +
src/view/uielement.js | 2 +-
src/view/view.js | 40 ++-
tests/view/{document => }/document.js | 4 +-
tests/view/document/integration.js | 83 ------
tests/view/domconverter/domconverter.js | 1 -
tests/view/observer/clickobserver.js | 11 +-
tests/view/observer/domeventdata.js | 16 +-
tests/view/observer/domeventobserver.js | 49 ++--
tests/view/observer/fakeselectionobserver.js | 13 +-
tests/view/observer/focusobserver.js | 38 +--
tests/view/observer/keyobserver.js | 11 +-
tests/view/observer/mouseobserver.js | 11 +-
tests/view/observer/mutationobserver.js | 57 +++--
tests/view/observer/observer.js | 7 +-
tests/view/observer/selectionobserver.js | 37 +--
tests/view/renderer.js | 48 ++--
.../jumpoverinlinefiller.js | 35 +--
.../{document => view}/jumpoveruielement.js | 21 +-
tests/view/{ => view}/view.js | 241 +++++++++++-------
31 files changed, 412 insertions(+), 409 deletions(-)
rename tests/view/{document => }/document.js (94%)
delete mode 100644 tests/view/document/integration.js
rename tests/view/{document => view}/jumpoverinlinefiller.js (87%)
rename tests/view/{document => view}/jumpoveruielement.js (96%)
rename tests/view/{ => view}/view.js (55%)
diff --git a/src/view/document.js b/src/view/document.js
index 73be180b1..f98989440 100644
--- a/src/view/document.js
+++ b/src/view/document.js
@@ -67,6 +67,18 @@ export default class Document {
* @member {Boolean} #isReadOnly
*/
this.set( 'isReadOnly', false );
+
+ /**
+ * True if document is focused.
+ *
+ * This property is updated by the {@link module:engine/view/observer/focusobserver~FocusObserver}.
+ * If the {@link module:engine/view/observer/focusobserver~FocusObserver} is disabled this property will not change.
+ *
+ * @readonly
+ * @observable
+ * @member {Boolean} module:engine/view/document~Document#isFocused
+ */
+ this.set( 'isFocused', false );
}
/**
diff --git a/src/view/observer/clickobserver.js b/src/view/observer/clickobserver.js
index c00c150fb..b16afd70f 100644
--- a/src/view/observer/clickobserver.js
+++ b/src/view/observer/clickobserver.js
@@ -19,8 +19,8 @@ import DomEventObserver from './domeventobserver';
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
export default class ClickObserver extends DomEventObserver {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
this.domEventType = 'click';
}
diff --git a/src/view/observer/domeventdata.js b/src/view/observer/domeventdata.js
index e50d0a7c6..deab9161c 100644
--- a/src/view/observer/domeventdata.js
+++ b/src/view/observer/domeventdata.js
@@ -16,18 +16,20 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
*/
export default class DomEventData {
/**
- * @param {module:engine/view/document~Document} document The instance of the tree view Document.
+ * @param {module:engine/view/view~view} view The instance of the tree view controller.
* @param {Event} domEvent The DOM event.
* @param {Object} [additionalData] Additional properties that the instance should contain.
*/
- constructor( document, domEvent, additionalData ) {
+ constructor( view, domEvent, additionalData ) {
+ this.view = view;
+
/**
* The instance of the document.
*
* @readonly
* @member {module:engine/view/document~Document} module:engine/view/observer/observer~Observer.DomEvent#view
*/
- this.document = document;
+ this.document = view.document;
/**
* The DOM event.
@@ -55,7 +57,7 @@ export default class DomEventData {
* @type module:engine/view/element~Element
*/
get target() {
- return this.document.domConverter.mapDomToView( this.domTarget );
+ return this.view.domConverter.mapDomToView( this.domTarget );
}
/**
diff --git a/src/view/observer/domeventobserver.js b/src/view/observer/domeventobserver.js
index 47c29020f..8b1b66b0f 100644
--- a/src/view/observer/domeventobserver.js
+++ b/src/view/observer/domeventobserver.js
@@ -55,8 +55,8 @@ export default class DomEventObserver extends Observer {
/**
* @inheritDoc
*/
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
/**
* If set to `true` DOM events will be listened on the capturing phase.
@@ -93,7 +93,7 @@ export default class DomEventObserver extends Observer {
*/
fire( eventType, domEvent, additionalData ) {
if ( this.isEnabled ) {
- this.document.fire( eventType, new DomEventData( this.document, domEvent, additionalData ) );
+ this.document.fire( eventType, new DomEventData( this.view, domEvent, additionalData ) );
}
}
}
diff --git a/src/view/observer/fakeselectionobserver.js b/src/view/observer/fakeselectionobserver.js
index 5f17b6417..be043c17d 100644
--- a/src/view/observer/fakeselectionobserver.js
+++ b/src/view/observer/fakeselectionobserver.js
@@ -25,10 +25,10 @@ export default class FakeSelectionObserver extends Observer {
/**
* Creates new FakeSelectionObserver instance.
*
- * @param {module:engine/view/document~Document} document
+ * @param {module:engine/view/view~View} view
*/
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
/**
* Fires debounced event `selectionChangeDone`. It uses `lodash#debounce` method to delay function call.
diff --git a/src/view/observer/focusobserver.js b/src/view/observer/focusobserver.js
index 7ef311d83..f3b1ffe6b 100644
--- a/src/view/observer/focusobserver.js
+++ b/src/view/observer/focusobserver.js
@@ -22,11 +22,12 @@ import DomEventObserver from './domeventobserver';
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
export default class FocusObserver extends DomEventObserver {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
this.domEventType = [ 'focus', 'blur' ];
this.useCapture = true;
+ const document = this.document;
document.on( 'focus', () => {
document.isFocused = true;
@@ -36,7 +37,7 @@ export default class FocusObserver extends DomEventObserver {
// overwrite new DOM selection with selection from the view.
// See https://github.com/ckeditor/ckeditor5-engine/issues/795 for more details.
// Long timeout is needed to solve #676 and https://github.com/ckeditor/ckeditor5-engine/issues/1157 issues.
- this._renderTimeoutId = setTimeout( () => document.render(), 50 );
+ this._renderTimeoutId = setTimeout( () => view.render(), 50 );
} );
document.on( 'blur', ( evt, data ) => {
@@ -46,7 +47,7 @@ export default class FocusObserver extends DomEventObserver {
document.isFocused = false;
// Re-render the document to update view elements.
- document.render();
+ view.render();
}
} );
diff --git a/src/view/observer/keyobserver.js b/src/view/observer/keyobserver.js
index 905c9af1b..f502f296a 100644
--- a/src/view/observer/keyobserver.js
+++ b/src/view/observer/keyobserver.js
@@ -18,8 +18,8 @@ import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
export default class KeyObserver extends DomEventObserver {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
this.domEventType = [ 'keydown', 'keyup' ];
}
diff --git a/src/view/observer/mouseobserver.js b/src/view/observer/mouseobserver.js
index 9a654ec58..21b3ccbb9 100644
--- a/src/view/observer/mouseobserver.js
+++ b/src/view/observer/mouseobserver.js
@@ -19,8 +19,8 @@ import DomEventObserver from './domeventobserver';
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
export default class MouseObserver extends DomEventObserver {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
this.domEventType = 'mousedown';
}
diff --git a/src/view/observer/mutationobserver.js b/src/view/observer/mutationobserver.js
index afc2bee7e..a4b57c721 100644
--- a/src/view/observer/mutationobserver.js
+++ b/src/view/observer/mutationobserver.js
@@ -31,8 +31,8 @@ import isEqualWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/isEqualWith';
* @extends module:engine/view/observer/observer~Observer
*/
export default class MutationObserver extends Observer {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
/**
* Native mutation observer config.
@@ -52,14 +52,14 @@ export default class MutationObserver extends Observer {
*
* @member {module:engine/view/domconverter~DomConverter}
*/
- this.domConverter = document.domConverter;
+ this.domConverter = view.domConverter;
/**
* Reference to the {@link module:engine/view/document~Document#renderer}.
*
* @member {module:engine/view/renderer~Renderer}
*/
- this.renderer = document.renderer;
+ this.renderer = view.renderer;
/**
* Observed DOM elements.
@@ -249,7 +249,7 @@ export default class MutationObserver extends Observer {
// If nothing changes on `mutations` event, at this point we have "dirty DOM" (changed) and de-synched
// view (which has not been changed). In order to "reset DOM" we render the view again.
- this.document.render();
+ this.view.render();
function sameNodes( child1, child2 ) {
// First level of comparison (array of children vs array of children) – use the Lodash's default behavior.
diff --git a/src/view/observer/observer.js b/src/view/observer/observer.js
index 75c9e77dc..690f5c279 100644
--- a/src/view/observer/observer.js
+++ b/src/view/observer/observer.js
@@ -21,16 +21,18 @@ export default class Observer {
/**
* Creates an instance of the observer.
*
- * @param {module:engine/view/document~Document} document
+ * @param {module:engine/view/view~View} view
*/
- constructor( document ) {
+ constructor( view ) {
+ this.view = view;
+
/**
* Reference to the {@link module:engine/view/document~Document} object.
*
* @readonly
* @member {module:engine/view/document~Document}
*/
- this.document = document;
+ this.document = view.document;
/**
* State of the observer. If it is disabled events will not be fired.
diff --git a/src/view/observer/selectionobserver.js b/src/view/observer/selectionobserver.js
index 54595c121..aa7339d1b 100644
--- a/src/view/observer/selectionobserver.js
+++ b/src/view/observer/selectionobserver.js
@@ -27,8 +27,8 @@ import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
* @extends module:engine/view/observer/observer~Observer
*/
export default class SelectionObserver extends Observer {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
/**
* Instance of the mutation observer. Selection observer calls
@@ -39,15 +39,7 @@ export default class SelectionObserver extends Observer {
* @member {module:engine/view/observer/mutationobserver~MutationObserver}
* module:engine/view/observer/selectionobserver~SelectionObserver#mutationObserver
*/
- this.mutationObserver = document.getObserver( MutationObserver );
-
- /**
- * Reference to the {@link module:engine/view/document~Document} object.
- *
- * @readonly
- * @member {module:engine/view/document~Document} module:engine/view/observer/selectionobserver~SelectionObserver#document
- */
- this.document = document;
+ this.mutationObserver = view.getObserver( MutationObserver );
/**
* Reference to the view {@link module:engine/view/selection~Selection} object used to compare new selection with it.
@@ -55,7 +47,7 @@ export default class SelectionObserver extends Observer {
* @readonly
* @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#selection
*/
- this.selection = document.selection;
+ this.selection = this.document.selection;
/* eslint-disable max-len */
/**
@@ -65,7 +57,7 @@ export default class SelectionObserver extends Observer {
* @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
*/
/* eslint-enable max-len */
- this.domConverter = document.domConverter;
+ this.domConverter = view.domConverter;
/**
* Set of documents which have added "selectionchange" listener to avoid adding listener twice to the same
@@ -172,7 +164,7 @@ export default class SelectionObserver extends Observer {
if ( this.selection.isSimilar( newViewSelection ) ) {
// If selection was equal and we are at this point of algorithm, it means that it was incorrect.
// Just re-render it, no need to fire any events, etc.
- this.document.render();
+ this.view.render();
} else {
const data = {
oldSelection: this.selection,
diff --git a/src/view/renderer.js b/src/view/renderer.js
index ac6d18826..24a1b3b77 100644
--- a/src/view/renderer.js
+++ b/src/view/renderer.js
@@ -109,6 +109,9 @@ export default class Renderer {
* @type {null|HTMLElement}
*/
this._fakeSelectionContainer = null;
+
+ // TODO: document rend er event.
+ this.decorate( 'render' );
}
/**
diff --git a/src/view/uielement.js b/src/view/uielement.js
index 29f99bb5b..9e5247489 100644
--- a/src/view/uielement.js
+++ b/src/view/uielement.js
@@ -93,7 +93,7 @@ export default class UIElement extends Element {
* @param {module:engine/view/view~View} view View controller to which the quirks handling will be injected.
*/
export function injectUiElementHandling( view ) {
- view.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, view.domConverter ) );
+ view.document.on( 'keydown', ( evt, data ) => jumpOverUiElement( evt, data, view.domConverter ) );
}
// Returns `null` because block filler is not needed for UIElements.
diff --git a/src/view/view.js b/src/view/view.js
index 4d5f7bd7d..a8f9b851b 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -31,7 +31,10 @@ export default class View {
this._writer = new Writer();
// TODO: check docs
- // TODO: move render event description to this file.
+ // TODO: move change event description to this file.
+ // TODO: check import path
+ // TODO: check where render() is used and eventually switch to change() where possible
+ // TODO: observers docs fixes
/**
* Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by
@@ -49,8 +52,8 @@ export default class View {
* @readonly
* @member {module:engine/view/renderer~Renderer} module:engine/view/view~View#renderer
*/
- this._renderer = new Renderer( this.domConverter, this.document.selection );
- this._renderer.bind( 'isFocused' ).to( this.document );
+ this.renderer = new Renderer( this.domConverter, this.document.selection );
+ this.renderer.bind( 'isFocused' ).to( this.document );
/**
* Roots of the DOM tree. Map on the `HTMLElement`s with roots names as keys.
@@ -68,18 +71,6 @@ export default class View {
*/
this._observers = new Map();
- /**
- * True if view is focused.
- *
- * This property is updated by the {@link module:engine/view/observer/focusobserver~FocusObserver}.
- * If the {@link module:engine/view/observer/focusobserver~FocusObserver} is disabled this property will not change.
- *
- * @readonly
- * @observable
- * @member {Boolean} module:engine/view/document~Document#isFocused
- */
- this.set( 'isFocused', false );
-
// Add default observers.
this.addObserver( MutationObserver );
this.addObserver( SelectionObserver );
@@ -113,12 +104,12 @@ export default class View {
this.domRoots.set( name, domRoot );
this.domConverter.bindElements( domRoot, viewRoot );
- this._renderer.markToSync( 'children', viewRoot );
- this._renderer.domDocuments.add( domRoot.ownerDocument );
+ this.renderer.markToSync( 'children', viewRoot );
+ this.renderer.domDocuments.add( domRoot.ownerDocument );
- viewRoot.on( 'change:children', ( evt, node ) => this._renderer.markToSync( 'children', node ) );
- viewRoot.on( 'change:attributes', ( evt, node ) => this._renderer.markToSync( 'attributes', node ) );
- viewRoot.on( 'change:text', ( evt, node ) => this._renderer.markToSync( 'text', node ) );
+ viewRoot.on( 'change:children', ( evt, node ) => this.renderer.markToSync( 'children', node ) );
+ viewRoot.on( 'change:attributes', ( evt, node ) => this.renderer.markToSync( 'attributes', node ) );
+ viewRoot.on( 'change:text', ( evt, node ) => this.renderer.markToSync( 'text', node ) );
for ( const observer of this._observers.values() ) {
observer.observe( domRoot, name );
@@ -217,7 +208,7 @@ export default class View {
*/
focus() {
if ( !this.document.isFocused ) {
- const editable = this.doocument.selection.editableElement;
+ const editable = this.document.selection.editableElement;
if ( editable ) {
this.domConverter.focus( editable );
@@ -278,7 +269,6 @@ export default class View {
observer.destroy();
}
- this.document.destroy();
this.stopListening();
}
@@ -291,9 +281,9 @@ export default class View {
_render() {
this._renderingInProgress = true;
- this.document.disableObservers();
- this._renderer.render();
- this.document.enableObservers();
+ this.disableObservers();
+ this.renderer.render();
+ this.enableObservers();
this._renderingInProgress = false;
}
diff --git a/tests/view/document/document.js b/tests/view/document.js
similarity index 94%
rename from tests/view/document/document.js
rename to tests/view/document.js
index 6c2d7a368..797e19d27 100644
--- a/tests/view/document/document.js
+++ b/tests/view/document.js
@@ -6,10 +6,10 @@
/* globals document */
import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-import Document from '../../../src/view/document';
+import Document from '../../src/view/document';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
import count from '@ckeditor/ckeditor5-utils/src/count';
-import createViewRoot from '../_utils/createroot';
+import createViewRoot from './_utils/createroot';
testUtils.createSinonSandbox();
diff --git a/tests/view/document/integration.js b/tests/view/document/integration.js
deleted file mode 100644
index b6bbad0eb..000000000
--- a/tests/view/document/integration.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/* globals document */
-
-import RootEditableElement from '../../../src/view/rooteditableelement';
-import Document from '../../../src/view/document';
-import ViewElement from '../../../src/view/element';
-import { isBlockFiller, BR_FILLER } from '../../../src/view/filler';
-
-import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
-
-describe( 'Document integration', () => {
- it( 'should remove content of the DOM', () => {
- const domDiv = createElement( document, 'div', { id: 'editor' }, [
- createElement( document, 'p' ),
- createElement( document, 'p' )
- ] );
-
- const viewDocument = new Document();
-
- createRoot( 'div', 'main', viewDocument );
- viewDocument.attachDomRoot( domDiv );
- viewDocument.render();
-
- expect( domDiv.childNodes.length ).to.equal( 1 );
- expect( isBlockFiller( domDiv.childNodes[ 0 ], BR_FILLER ) ).to.be.true;
-
- viewDocument.destroy();
- } );
-
- it( 'should render changes in the Document', () => {
- const domDiv = document.createElement( 'div' );
-
- const viewDocument = new Document();
- createRoot( 'div', 'main', viewDocument );
- viewDocument.attachDomRoot( domDiv );
-
- viewDocument.getRoot().appendChildren( new ViewElement( 'p' ) );
- viewDocument.render();
-
- expect( domDiv.childNodes.length ).to.equal( 1 );
- expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' );
-
- viewDocument.destroy();
- } );
-
- it( 'should render attribute changes', () => {
- const domRoot = document.createElement( 'div' );
-
- const viewDocument = new Document();
- const viewRoot = createRoot( 'div', 'main', viewDocument );
-
- viewDocument.attachDomRoot( domRoot );
-
- const viewP = new ViewElement( 'p', { class: 'foo' } );
- viewRoot.appendChildren( viewP );
- viewDocument.render();
-
- expect( domRoot.childNodes.length ).to.equal( 1 );
- expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' );
-
- viewP.setAttribute( 'class', 'bar' );
- viewDocument.render();
-
- expect( domRoot.childNodes.length ).to.equal( 1 );
- expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' );
-
- viewDocument.destroy();
- } );
-} );
-
-function createRoot( name, rootName, viewDoc ) {
- const viewRoot = new RootEditableElement( name );
-
- viewRoot.rootName = rootName;
- viewRoot.document = viewDoc;
- viewDoc.roots.add( viewRoot );
-
- return viewRoot;
-}
diff --git a/tests/view/domconverter/domconverter.js b/tests/view/domconverter/domconverter.js
index 524bbd748..252ccffb9 100644
--- a/tests/view/domconverter/domconverter.js
+++ b/tests/view/domconverter/domconverter.js
@@ -53,7 +53,6 @@ describe( 'DomConverter', () => {
afterEach( () => {
converter.unbindDomElement( domEditable );
document.body.removeChild( domEditableParent );
- viewDocument.destroy();
document.body.focus();
} );
diff --git a/tests/view/observer/clickobserver.js b/tests/view/observer/clickobserver.js
index 8f8bc5985..d5f908fee 100644
--- a/tests/view/observer/clickobserver.js
+++ b/tests/view/observer/clickobserver.js
@@ -6,18 +6,19 @@
/* globals document */
import ClickObserver from '../../../src/view/observer/clickobserver';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
describe( 'ClickObserver', () => {
- let viewDocument, observer;
+ let view, viewDocument, observer;
beforeEach( () => {
- viewDocument = new ViewDocument();
- observer = viewDocument.addObserver( ClickObserver );
+ view = new View();
+ viewDocument = view.document;
+ observer = view.addObserver( ClickObserver );
} );
afterEach( () => {
- viewDocument.destroy();
+ view.destroy();
} );
it( 'should define domEventType', () => {
diff --git a/tests/view/observer/domeventdata.js b/tests/view/observer/domeventdata.js
index 7da4968c3..73dce01e3 100644
--- a/tests/view/observer/domeventdata.js
+++ b/tests/view/observer/domeventdata.js
@@ -6,31 +6,33 @@
/* globals document */
import DomEventData from '../../../src/view/observer/domeventdata';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
describe( 'DomEventData', () => {
- let viewDocument, viewBody, domRoot;
+ let view, viewDocument, viewBody, domRoot;
beforeEach( () => {
- viewDocument = new ViewDocument();
+ view = new View();
+ viewDocument = view.document;
domRoot = document.createElement( 'div' );
domRoot.innerHTML = '';
document.body.appendChild( domRoot );
- viewBody = viewDocument.domConverter.domToView( document.body, { bind: true } );
+ viewBody = view.domConverter.domToView( document.body, { bind: true } );
} );
afterEach( () => {
domRoot.parentElement.removeChild( domRoot );
- viewDocument.destroy();
+ view.destroy();
} );
describe( 'constructor()', () => {
it( 'sets properties', () => {
const domEvt = { target: document.body };
- const data = new DomEventData( viewDocument, domEvt, { foo: 1, bar: true } );
+ const data = new DomEventData( view, domEvt, { foo: 1, bar: true } );
+ expect( data ).to.have.property( 'view', view );
expect( data ).to.have.property( 'document', viewDocument );
expect( data ).to.have.property( 'domEvent', domEvt );
expect( data ).to.have.property( 'domTarget', document.body );
@@ -43,7 +45,7 @@ describe( 'DomEventData', () => {
describe( 'target', () => {
it( 'returns bound element', () => {
const domEvt = { target: document.body };
- const data = new DomEventData( viewDocument, domEvt );
+ const data = new DomEventData( view, domEvt );
expect( data ).to.have.property( 'target', viewBody );
} );
diff --git a/tests/view/observer/domeventobserver.js b/tests/view/observer/domeventobserver.js
index c4732bc90..23cd0ba31 100644
--- a/tests/view/observer/domeventobserver.js
+++ b/tests/view/observer/domeventobserver.js
@@ -7,13 +7,13 @@
import DomEventObserver from '../../../src/view/observer/domeventobserver';
import Observer from '../../../src/view/observer/observer';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
import UIElement from '../../../src/view/uielement';
import createViewRoot from '../_utils/createroot';
class ClickObserver extends DomEventObserver {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
this.domEventType = 'click';
}
@@ -24,8 +24,8 @@ class ClickObserver extends DomEventObserver {
}
class MultiObserver extends DomEventObserver {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
this.domEventType = [ 'evt1', 'evt2' ];
}
@@ -36,22 +36,23 @@ class MultiObserver extends DomEventObserver {
}
class ClickCapturingObserver extends ClickObserver {
- constructor( document ) {
- super( document );
+ constructor( view ) {
+ super( view );
this.useCapture = true;
}
}
describe( 'DomEventObserver', () => {
- let viewDocument;
+ let view, viewDocument;
beforeEach( () => {
- viewDocument = new ViewDocument();
+ view = new View();
+ viewDocument = view.document;
} );
afterEach( () => {
- viewDocument.destroy();
+ view.destroy();
} );
describe( 'constructor()', () => {
@@ -68,8 +69,8 @@ describe( 'DomEventObserver', () => {
const evtSpy = sinon.spy();
createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domElement );
- viewDocument.addObserver( ClickObserver );
+ view.attachDomRoot( domElement );
+ view.addObserver( ClickObserver );
viewDocument.on( 'click', evtSpy );
domElement.dispatchEvent( domEvent );
@@ -91,8 +92,8 @@ describe( 'DomEventObserver', () => {
const evtSpy2 = sinon.spy();
createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domElement );
- viewDocument.addObserver( MultiObserver );
+ view.attachDomRoot( domElement );
+ view.addObserver( MultiObserver );
viewDocument.on( 'evt1', evtSpy1 );
viewDocument.on( 'evt2', evtSpy2 );
@@ -109,8 +110,8 @@ describe( 'DomEventObserver', () => {
const evtSpy = sinon.spy();
createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domElement );
- const testObserver = viewDocument.addObserver( ClickObserver );
+ view.attachDomRoot( domElement );
+ const testObserver = view.addObserver( ClickObserver );
viewDocument.on( 'click', evtSpy );
testObserver.disable();
@@ -126,8 +127,8 @@ describe( 'DomEventObserver', () => {
const evtSpy = sinon.spy();
createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domElement );
- const testObserver = viewDocument.addObserver( ClickObserver );
+ view.attachDomRoot( domElement );
+ const testObserver = view.addObserver( ClickObserver );
viewDocument.on( 'click', evtSpy );
testObserver.disable();
@@ -149,8 +150,8 @@ describe( 'DomEventObserver', () => {
const domEvent = new MouseEvent( 'click' );
domElement.appendChild( childDomElement );
createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domElement );
- viewDocument.addObserver( ClickCapturingObserver );
+ view.attachDomRoot( domElement );
+ view.addObserver( ClickCapturingObserver );
viewDocument.on( 'click', ( evt, domEventData ) => {
expect( domEventData.domEvent.eventPhase ).to.equal( domEventData.domEvent.CAPTURING_PHASE );
@@ -175,14 +176,14 @@ describe( 'DomEventObserver', () => {
beforeEach( () => {
domRoot = document.createElement( 'div' );
const viewRoot = createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domRoot );
+ view.attachDomRoot( domRoot );
uiElement = new MyUIElement( 'p' );
viewRoot.appendChildren( uiElement );
- viewDocument.render();
+ view.render();
domEvent = new MouseEvent( 'click', { bubbles: true } );
evtSpy = sinon.spy();
- viewDocument.addObserver( ClickObserver );
+ view.addObserver( ClickObserver );
viewDocument.on( 'click', evtSpy );
} );
@@ -209,7 +210,7 @@ describe( 'DomEventObserver', () => {
describe( 'fire', () => {
it( 'should do nothing if observer is disabled', () => {
- const testObserver = new ClickObserver( viewDocument );
+ const testObserver = new ClickObserver( view );
const fireSpy = sinon.spy( viewDocument, 'fire' );
testObserver.disable();
diff --git a/tests/view/observer/fakeselectionobserver.js b/tests/view/observer/fakeselectionobserver.js
index 290d4154d..85cbaa658 100644
--- a/tests/view/observer/fakeselectionobserver.js
+++ b/tests/view/observer/fakeselectionobserver.js
@@ -7,14 +7,14 @@
import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement';
import FakeSelectionObserver from '../../../src/view/observer/fakeselectionobserver';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
import DomEventData from '../../../src/view/observer/domeventdata';
import createViewRoot from '../_utils/createroot';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
import { setData, stringify } from '../../../src/dev-utils/view';
describe( 'FakeSelectionObserver', () => {
- let observer, viewDocument, root, domRoot;
+ let observer, view, viewDocument, root, domRoot;
before( () => {
domRoot = createElement( document, 'div', {
@@ -28,15 +28,16 @@ describe( 'FakeSelectionObserver', () => {
} );
beforeEach( () => {
- viewDocument = new ViewDocument();
+ view = new View();
+ viewDocument = view.document;
root = createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domRoot );
- observer = viewDocument.getObserver( FakeSelectionObserver );
+ view.attachDomRoot( domRoot );
+ observer = view.getObserver( FakeSelectionObserver );
viewDocument.selection.setFake();
} );
afterEach( () => {
- viewDocument.destroy();
+ view.destroy();
} );
it( 'should do nothing if selection is not fake', () => {
diff --git a/tests/view/observer/focusobserver.js b/tests/view/observer/focusobserver.js
index 1d2c2eda6..0a3b4d621 100644
--- a/tests/view/observer/focusobserver.js
+++ b/tests/view/observer/focusobserver.js
@@ -5,21 +5,22 @@
/* globals document */
import FocusObserver from '../../../src/view/observer/focusobserver';
-import ViewDocument from '../../../src/view/document';
+import View from '../../../src/view/view';
import ViewRange from '../../../src/view/range';
import createViewRoot from '../_utils/createroot';
import { setData } from '../../../src/dev-utils/view';
describe( 'FocusObserver', () => {
- let viewDocument, observer;
+ let view, viewDocument, observer;
beforeEach( () => {
- viewDocument = new ViewDocument();
- observer = viewDocument.getObserver( FocusObserver );
+ view = new View();
+ viewDocument = view.document;
+ observer = view.getObserver( FocusObserver );
} );
afterEach( () => {
- viewDocument.destroy();
+ view.destroy();
} );
it( 'should define domEventType', () => {
@@ -58,7 +59,7 @@ describe( 'FocusObserver', () => {
} );
it( 'should render document after blurring', () => {
- const renderSpy = sinon.spy( viewDocument, 'render' );
+ const renderSpy = sinon.spy( view, 'render' );
observer.onDomEvent( { type: 'blur', target: document.body } );
@@ -74,7 +75,7 @@ describe( 'FocusObserver', () => {
domHeader = document.createElement( 'h1' );
viewMain = createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domMain );
+ view.attachDomRoot( domMain );
} );
it( 'should set isFocused to true on focus', () => {
@@ -118,7 +119,7 @@ describe( 'FocusObserver', () => {
} );
it( 'should delay rendering by 50ms', () => {
- const renderSpy = sinon.spy( viewDocument, 'render' );
+ const renderSpy = sinon.spy( view, 'render' );
const clock = sinon.useFakeTimers();
observer.onDomEvent( { type: 'focus', target: domMain } );
@@ -130,7 +131,7 @@ describe( 'FocusObserver', () => {
} );
it( 'should not call render if destroyed', () => {
- const renderSpy = sinon.spy( viewDocument, 'render' );
+ const renderSpy = sinon.spy( view, 'render' );
const clock = sinon.useFakeTimers();
observer.onDomEvent( { type: 'focus', target: domMain } );
@@ -150,11 +151,12 @@ describe( 'FocusObserver', () => {
domRoot = document.createElement( 'div' );
document.body.appendChild( domRoot );
- viewDocument = new ViewDocument();
+ view = new View();
+ viewDocument = view.document;
createViewRoot( viewDocument );
- viewDocument.attachDomRoot( domRoot );
+ view.attachDomRoot( domRoot );
- observer = viewDocument.getObserver( FocusObserver );
+ observer = view.getObserver( FocusObserver );
} );
it( 'should always render document after selectionChange event', done => {
@@ -162,12 +164,12 @@ describe( 'FocusObserver', () => {
const renderSpy = sinon.spy();
setData( viewDocument, '
' );
- viewDocument.render();
+ view.render();
} );
it( 'should allow to add placeholder to elements from different documents', () => {
setData( viewDocument, '
' );
const element = viewRoot.getChild( 0 );
attachPlaceholder( element, 'foo bar baz' );
@@ -40,7 +40,7 @@ describe( 'placeholder', () => {
} );
it( 'if element has children set only data attribute', () => {
- setData( viewDocument, '
first div
{another div}
' );
+ setData( view, '
first div
{another div}
' );
const element = viewRoot.getChild( 0 );
attachPlaceholder( element, 'foo bar baz' );
@@ -50,7 +50,7 @@ describe( 'placeholder', () => {
} );
it( 'if element has only ui elements, set CSS class and data attribute', () => {
- setData( viewDocument, '
{another div}
' );
+ setData( view, '
{another div}
' );
const element = viewRoot.getChild( 0 );
attachPlaceholder( element, 'foo bar baz' );
@@ -60,7 +60,7 @@ describe( 'placeholder', () => {
} );
it( 'if element has selection inside set only data attribute', () => {
- setData( viewDocument, '
[]
another div
' );
+ setData( view, '
[]
another div
' );
const element = viewRoot.getChild( 0 );
attachPlaceholder( element, 'foo bar baz' );
@@ -70,7 +70,7 @@ describe( 'placeholder', () => {
} );
it( 'if element has selection inside but document is blurred should contain placeholder CSS class', () => {
- setData( viewDocument, '
[]
another div
' );
+ setData( view, '
[]
another div
' );
const element = viewRoot.getChild( 0 );
viewDocument.isFocused = false;
@@ -81,7 +81,7 @@ describe( 'placeholder', () => {
} );
it( 'use check function if one is provided', () => {
- setData( viewDocument, '
{another div}
' );
+ setData( view, '
{another div}
' );
const element = viewRoot.getChild( 0 );
const spy = sinon.spy( () => false );
@@ -93,7 +93,7 @@ describe( 'placeholder', () => {
} );
it( 'should remove CSS class if selection is moved inside', () => {
- setData( viewDocument, '
{another div}
' );
+ setData( view, '
{another div}
' );
const element = viewRoot.getChild( 0 );
attachPlaceholder( element, 'foo bar baz' );
@@ -109,7 +109,7 @@ describe( 'placeholder', () => {
} );
it( 'should change placeholder settings when called twice', () => {
- setData( viewDocument, '
{another div}
' );
+ setData( view, '
{another div}
' );
const element = viewRoot.getChild( 0 );
attachPlaceholder( element, 'foo bar baz' );
@@ -120,24 +120,24 @@ describe( 'placeholder', () => {
} );
it( 'should not throw when element is no longer in document', () => {
- setData( viewDocument, '
{another div}
' );
+ setData( view, '
{another div}
' );
const element = viewRoot.getChild( 0 );
attachPlaceholder( element, 'foo bar baz' );
- setData( viewDocument, '
paragraph
' );
+ setData( view, '
paragraph
' );
view.render();
} );
it( 'should allow to add placeholder to elements from different documents', () => {
- setData( viewDocument, '
' );
// Re-render changes in view to DOM.
view.render();
@@ -1938,7 +1938,7 @@ describe( 'Renderer', () => {
} );
it( 'should properly render if text is changed and element is inserted into same node #1', () => {
- setViewData( viewDoc,
+ setViewData( view,
'foo'
);
@@ -1949,7 +1949,7 @@ describe( 'Renderer', () => {
const textNode = viewRoot.getChild( 0 ).getChild( 0 );
textNode.data = 'foobar';
writer.insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) );
- expect( getViewData( viewDoc ) ).to.equal( '
foobar
' );
+ expect( getViewData( view ) ).to.equal( '
foobar
' );
// Re-render changes in view to DOM.
view.render();
@@ -1960,7 +1960,7 @@ describe( 'Renderer', () => {
} );
it( 'should properly render if text is changed and element is inserted into same node #2', () => {
- setViewData( viewDoc,
+ setViewData( view,
'foo'
);
@@ -1971,7 +1971,7 @@ describe( 'Renderer', () => {
const textNode = viewRoot.getChild( 0 ).getChild( 0 );
textNode.data = 'foobar';
writer.insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) );
- expect( getViewData( viewDoc ) ).to.equal( '
foobar
' );
+ expect( getViewData( view ) ).to.equal( '
foobar
' );
// Re-render changes in view to DOM.
view.render();
@@ -1982,7 +1982,7 @@ describe( 'Renderer', () => {
} );
it( 'should not unbind elements that are removed and reinserted to DOM', () => {
- setViewData( viewDoc,
+ setViewData( view,
'' +
'' +
'' +
@@ -1999,7 +1999,7 @@ describe( 'Renderer', () => {
writer.remove( ViewRange.createOn( firstElement ) );
writer.insert( new ViewPosition( container, 2 ), firstElement );
- expect( getViewData( viewDoc ) ).to.equal( '
' );
+ expect( getViewData( view ) ).to.equal( '
' );
// Re-render changes in view to DOM.
view.render();
diff --git a/tests/view/view/jumpoverinlinefiller.js b/tests/view/view/jumpoverinlinefiller.js
index d1653e39d..6f9f69d5d 100644
--- a/tests/view/view/jumpoverinlinefiller.js
+++ b/tests/view/view/jumpoverinlinefiller.js
@@ -42,7 +42,7 @@ describe( 'View', () => {
describe( 'jump over inline filler hack', () => {
it( 'should jump over inline filler when left arrow is pressed after inline filler', () => {
- setData( viewDocument, 'foo[]bar' );
+ setData( view, 'foo[]bar' );
view.render();
viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } );
@@ -64,7 +64,7 @@ describe( 'View', () => {
} );
it( 'should do nothing when another key is pressed', () => {
- setData( viewDocument, 'foo[]bar' );
+ setData( view, 'foo[]bar' );
view.render();
viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: view.domRoots.get( 'main' ) } );
@@ -77,7 +77,7 @@ describe( 'View', () => {
} );
it( 'should do nothing if range is not collapsed', () => {
- setData( viewDocument, 'foo{x}bar' );
+ setData( view, 'foo{x}bar' );
view.render();
viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } );
@@ -92,7 +92,7 @@ describe( 'View', () => {
// See #664
// it( 'should do nothing if node does not start with the filler', () => {
- // setData( viewDocument, 'foo{}xbar' );
+ // setData( view, 'foo{}xbar' );
// viewDocument.render();
// viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } );
@@ -105,7 +105,7 @@ describe( 'View', () => {
// } );
it( 'should do nothing if caret is not directly before the filler', () => {
- setData( viewDocument, 'foo[]bar' );
+ setData( view, 'foo[]bar' );
view.render();
// Insert a letter to the : 'foox{}bar'
diff --git a/tests/view/view/jumpoveruielement.js b/tests/view/view/jumpoveruielement.js
index 03cac5e48..8e2d4920f 100644
--- a/tests/view/view/jumpoveruielement.js
+++ b/tests/view/view/jumpoveruielement.js
@@ -290,42 +290,42 @@ describe( 'Document', () => {
} );
it( 'do nothing if selection is not directly before ui element', () => {
- setViewData( viewDocument, 'fo{}obar' );
+ setViewData( view, 'fo{}obar' );
renderAndFireKeydownEvent();
check( 'foo', 2 );
} );
it( 'do nothing if selection is in attribute element but not before ui element', () => {
- setViewData( viewDocument, 'foo{}bar' );
+ setViewData( view, 'foo{}bar' );
renderAndFireKeydownEvent();
check( 'foo', 3 );
} );
it( 'do nothing if selection is before non-empty attribute element', () => {
- setViewData( viewDocument, 'fo{}obar' );
+ setViewData( view, 'fo{}obar' );
renderAndFireKeydownEvent();
check( 'fo', 2 );
} );
it( 'do nothing if selection is before container element - case 1', () => {
- setViewData( viewDocument, 'foo{}bar' );
+ setViewData( view, 'foo{}bar' );
renderAndFireKeydownEvent();
check( 'foo', 3 );
} );
it( 'do nothing if selection is before container element - case 2', () => {
- setViewData( viewDocument, 'foo{}' );
+ setViewData( view, 'foo{}' );
renderAndFireKeydownEvent();
check( 'foo', 3 );
} );
it( 'do nothing if selection is at the end of last container element', () => {
- setViewData( viewDocument, 'foo{}' );
+ setViewData( view, 'foo{}' );
renderAndFireKeydownEvent();
check( 'foo', 3 );
@@ -334,14 +334,14 @@ describe( 'Document', () => {
describe( 'non-collapsed selection', () => {
it( 'should do nothing', () => {
- setViewData( viewDocument, 'f{oo}bar' );
+ setViewData( view, 'f{oo}bar' );
renderAndFireKeydownEvent();
check( 'foo', 1, 'foo', 3 );
} );
it( 'should do nothing if selection is not before ui element - shift key pressed', () => {
- setViewData( viewDocument, 'f{o}obar' );
+ setViewData( view, 'f{o}obar' );
renderAndFireKeydownEvent( { shiftKey: true } );
check( 'foo', 1, 'foo', 2 );
From 5d9fcc521981a1177ece06f58c1ba04089c4150e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Tue, 23 Jan 2018 16:42:25 +0100
Subject: [PATCH 22/89] Fixed docs after view controller creation.
---
src/conversion/modelconversiondispatcher.js | 12 ++++++-
src/view/document.js | 18 ++--------
src/view/observer/clickobserver.js | 6 ++--
src/view/observer/domeventdata.js | 2 +-
src/view/observer/focusobserver.js | 8 ++---
src/view/observer/keyobserver.js | 8 ++---
src/view/observer/mouseobserver.js | 5 ++-
src/view/observer/mutationobserver.js | 7 ++--
src/view/observer/observer.js | 6 ++--
src/view/observer/selectionobserver.js | 10 +++---
src/view/renderer.js | 7 ++++
src/view/view.js | 25 ++++++++++++-
src/view/writer.js | 40 ++++++++++-----------
13 files changed, 84 insertions(+), 70 deletions(-)
diff --git a/src/conversion/modelconversiondispatcher.js b/src/conversion/modelconversiondispatcher.js
index f5dc7c6df..4f191349c 100644
--- a/src/conversion/modelconversiondispatcher.js
+++ b/src/conversion/modelconversiondispatcher.js
@@ -73,6 +73,9 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
* {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed} a value from a consumable and
* converted the change should also stop the event (for efficiency purposes).
*
+ * When providing custom listeners for `ModelConversionDispatcher` remember to use provided
+ * {@link module:engine/view/writer~Writer view writer} to apply changes to the view document.
+ *
* Example of a custom converter for `ModelConversionDispatcher`:
*
* // We will convert inserting "paragraph" model element into the model.
@@ -92,7 +95,7 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
* conversionApi.mapper.bindElements( data.item, viewElement );
*
* // Add the newly created view element to the view.
- * viewWriter.insert( viewPosition, viewElement );
+ * conversionApi.writer.insert( viewPosition, viewElement );
*
* // Remember to stop the event propagation.
* evt.stop();
@@ -126,6 +129,7 @@ export default class ModelConversionDispatcher {
* Takes {@link module:engine/model/differ~Differ model differ} object with buffered changes and fires conversion basing on it.
*
* @param {module:engine/model/differ~Differ} differ Differ object with buffered changes.
+ * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
*/
convertChanges( differ, writer ) {
this.conversionApi.writer = writer;
@@ -157,6 +161,7 @@ export default class ModelConversionDispatcher {
* @fires insert
* @fires attribute
* @param {module:engine/model/range~Range} range Inserted range.
+ * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
*/
convertInsert( range, writer ) {
this.conversionApi.writer = writer;
@@ -194,6 +199,7 @@ export default class ModelConversionDispatcher {
* @param {module:engine/model/position~Position} position Position from which node was removed.
* @param {Number} length Offset size of removed node.
* @param {String} name Name of removed node.
+ * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
*/
convertRemove( position, length, name, writer ) {
this.conversionApi.writer = writer;
@@ -211,6 +217,7 @@ export default class ModelConversionDispatcher {
* @param {String} key Key of the attribute that has changed.
* @param {*} oldValue Attribute value before the change or `null` if the attribute has not been set before.
* @param {*} newValue New attribute value or `null` if the attribute has been removed.
+ * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
*/
convertAttribute( range, key, oldValue, newValue, writer ) {
this.conversionApi.writer = writer;
@@ -243,6 +250,7 @@ export default class ModelConversionDispatcher {
* @fires addMarker
* @fires attribute
* @param {module:engine/model/selection~Selection} selection Selection to convert.
+ * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
*/
convertSelection( selection, writer ) {
this.conversionApi.writer = writer;
@@ -296,6 +304,7 @@ export default class ModelConversionDispatcher {
* @fires addMarker
* @param {String} markerName Marker name.
* @param {module:engine/model/range~Range} markerRange Marker range.
+ * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
*/
convertMarkerAdd( markerName, markerRange, writer ) {
// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
@@ -343,6 +352,7 @@ export default class ModelConversionDispatcher {
* @fires removeMarker
* @param {String} markerName Marker name.
* @param {module:engine/model/range~Range} markerRange Marker range.
+ * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document.
*/
convertMarkerRemove( markerName, markerRange, writer ) {
// Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment).
diff --git a/src/view/document.js b/src/view/document.js
index f98989440..8525f11f4 100644
--- a/src/view/document.js
+++ b/src/view/document.js
@@ -12,23 +12,9 @@ import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-// todo: check the docs
/**
- * Document class creates an abstract layer over the content editable area.
- * It combines the actual tree of view elements, tree of DOM elements,
- * {@link module:engine/view/domconverter~DomConverter DOM Converter}, {@link module:engine/view/renderer~Renderer renderer} and all
- * {@link module:engine/view/observer/observer~Observer observers}.
- *
- * If you want to only transform the tree of view elements to the DOM elements you can use the
- * {@link module:engine/view/domconverter~DomConverter DomConverter}.
- *
- * Note that the following observers are added by the class constructor and are always available:
- *
- * * {@link module:engine/view/observer/mutationobserver~MutationObserver},
- * * {@link module:engine/view/observer/selectionobserver~SelectionObserver},
- * * {@link module:engine/view/observer/focusobserver~FocusObserver},
- * * {@link module:engine/view/observer/keyobserver~KeyObserver},
- * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
+ * Document class creates an abstract layer over the content editable area, contains a tree of view elements and
+ * {@link module:engine/view/selection~Selection view selection} associated with this document.
*
* @mixes module:utils/observablemixin~ObservableMixin
*/
diff --git a/src/view/observer/clickobserver.js b/src/view/observer/clickobserver.js
index b16afd70f..72d19b5c9 100644
--- a/src/view/observer/clickobserver.js
+++ b/src/view/observer/clickobserver.js
@@ -13,8 +13,8 @@ import DomEventObserver from './domeventobserver';
* {@link module:engine/view/document~Document#event:click Click} event observer.
*
* Note that this observer is not available by default. To make it available it needs to be added to
- * {@link module:engine/view/document~Document}
- * by a {@link module:engine/view/document~Document#addObserver} method.
+ * {@link module:engine/view/view~View view controller}
+ * by a {@link module:engine/view/view~View#addObserver} method.
*
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
@@ -37,7 +37,7 @@ export default class ClickObserver extends DomEventObserver {
*
* Note that this event is not available by default. To make it available
* {@link module:engine/view/observer/clickobserver~ClickObserver} needs to be added
- * to {@link module:engine/view/document~Document} by a {@link module:engine/view/document~Document#addObserver} method.
+ * to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method.
*
* @see module:engine/view/observer/clickobserver~ClickObserver
* @event module:engine/view/document~Document#event:click
diff --git a/src/view/observer/domeventdata.js b/src/view/observer/domeventdata.js
index deab9161c..1460eca81 100644
--- a/src/view/observer/domeventdata.js
+++ b/src/view/observer/domeventdata.js
@@ -16,7 +16,7 @@ import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
*/
export default class DomEventData {
/**
- * @param {module:engine/view/view~view} view The instance of the tree view controller.
+ * @param {module:engine/view/view~view} view The instance of the view controller.
* @param {Event} domEvent The DOM event.
* @param {Object} [additionalData] Additional properties that the instance should contain.
*/
diff --git a/src/view/observer/focusobserver.js b/src/view/observer/focusobserver.js
index f3b1ffe6b..24e66967a 100644
--- a/src/view/observer/focusobserver.js
+++ b/src/view/observer/focusobserver.js
@@ -17,7 +17,7 @@ import DomEventObserver from './domeventobserver';
* Focus observer handle also {@link module:engine/view/rooteditableelement~RootEditableElement#isFocused isFocused} property of the
* {@link module:engine/view/rooteditableelement~RootEditableElement root elements}.
*
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
*
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
@@ -81,8 +81,7 @@ export default class FocusObserver extends DomEventObserver {
* Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
*
* Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/focusobserver~FocusObserver
* @event module:engine/view/document~Document#event:focus
@@ -95,8 +94,7 @@ export default class FocusObserver extends DomEventObserver {
* Introduced by {@link module:engine/view/observer/focusobserver~FocusObserver}.
*
* Note that because {@link module:engine/view/observer/focusobserver~FocusObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/focusobserver~FocusObserver
* @event module:engine/view/document~Document#event:blur
diff --git a/src/view/observer/keyobserver.js b/src/view/observer/keyobserver.js
index f502f296a..94d512f68 100644
--- a/src/view/observer/keyobserver.js
+++ b/src/view/observer/keyobserver.js
@@ -13,7 +13,7 @@ import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
/**
* {@link module:engine/view/document~Document#event:keydown Key down} event observer.
*
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
*
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
@@ -45,8 +45,7 @@ export default class KeyObserver extends DomEventObserver {
* Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
*
* Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/keyobserver~KeyObserver
* @event module:engine/view/document~Document#event:keydown
@@ -59,8 +58,7 @@ export default class KeyObserver extends DomEventObserver {
* Introduced by {@link module:engine/view/observer/keyobserver~KeyObserver}.
*
* Note that because {@link module:engine/view/observer/keyobserver~KeyObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/keyobserver~KeyObserver
* @event module:engine/view/document~Document#event:keyup
diff --git a/src/view/observer/mouseobserver.js b/src/view/observer/mouseobserver.js
index 21b3ccbb9..791091f78 100644
--- a/src/view/observer/mouseobserver.js
+++ b/src/view/observer/mouseobserver.js
@@ -13,8 +13,7 @@ import DomEventObserver from './domeventobserver';
* Mouse events observer.
*
* Note that this observer is not available by default. To make it available it needs to be added to
- * {@link module:engine/view/document~Document}
- * by {@link module:engine/view/document~Document#addObserver} method.
+ * {@link module:engine/view/view~View} by {@link module:engine/view/view~View#addObserver} method.
*
* @extends module:engine/view/observer/domeventobserver~DomEventObserver
*/
@@ -36,7 +35,7 @@ export default class MouseObserver extends DomEventObserver {
* Introduced by {@link module:engine/view/observer/mouseobserver~MouseObserver}.
*
* Note that this event is not available by default. To make it available {@link module:engine/view/observer/mouseobserver~MouseObserver}
- * needs to be added to {@link module:engine/view/document~Document} by a {@link module:engine/view/document~Document#addObserver} method.
+ * needs to be added to {@link module:engine/view/view~View} by a {@link module:engine/view/view~View#addObserver} method.
*
* @see module:engine/view/observer/mouseobserver~MouseObserver
* @event module:engine/view/document~Document#event:mousedown
diff --git a/src/view/observer/mutationobserver.js b/src/view/observer/mutationobserver.js
index a4b57c721..255c6ea25 100644
--- a/src/view/observer/mutationobserver.js
+++ b/src/view/observer/mutationobserver.js
@@ -26,7 +26,7 @@ import isEqualWith from '@ckeditor/ckeditor5-utils/src/lib/lodash/isEqualWith';
* mutations on elements which do not have corresponding view elements. Also
* {@link module:engine/view/observer/mutationobserver~MutatedText text mutation} is fired only if parent element do not change child list.
*
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
*
* @extends module:engine/view/observer/observer~Observer
*/
@@ -295,14 +295,13 @@ export default class MutationObserver extends Observer {
}
/**
- * Fired when mutation occurred. If tree view is not changed on this event, DOM will be reverter to the state before
+ * Fired when mutation occurred. If tree view is not changed on this event, DOM will be reverted to the state before
* mutation, so all changes which should be applied, should be handled on this event.
*
* Introduced by {@link module:engine/view/observer/mutationobserver~MutationObserver}.
*
* Note that because {@link module:engine/view/observer/mutationobserver~MutationObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/mutationobserver~MutationObserver
* @event module:engine/view/document~Document#event:mutations
diff --git a/src/view/observer/observer.js b/src/view/observer/observer.js
index 690f5c279..a47ece19c 100644
--- a/src/view/observer/observer.js
+++ b/src/view/observer/observer.js
@@ -44,8 +44,8 @@ export default class Observer {
}
/**
- * Enables the observer. This method is called when then observer is registered to the
- * {@link module:engine/view/document~Document} and after {@link module:engine/view/document~Document#render rendering}
+ * Enables the observer. This method is called when the observer is registered to the
+ * {@link module:engine/view/view~View} and after {@link module:engine/view/view~View#render rendering}
* (all observers are {@link #disable disabled} before rendering).
*
* A typical use case for disabling observers is that mutation observers need to be disabled for the rendering.
@@ -59,7 +59,7 @@ export default class Observer {
/**
* Disables the observer. This method is called before
- * {@link module:engine/view/document~Document#render rendering} to prevent firing events during rendering.
+ * {@link module:engine/view/view~View#render rendering} to prevent firing events during rendering.
*
* @see module:engine/view/observer/observer~Observer#enable
*/
diff --git a/src/view/observer/selectionobserver.js b/src/view/observer/selectionobserver.js
index aa7339d1b..7947508d4 100644
--- a/src/view/observer/selectionobserver.js
+++ b/src/view/observer/selectionobserver.js
@@ -21,7 +21,7 @@ import debounce from '@ckeditor/ckeditor5-utils/src/lib/lodash/debounce';
* {@link module:engine/view/document~Document#event:selectionChange} event only if selection change was the only change in the document
* and DOM selection is different then the view selection.
*
- * Note that this observer is attached by the {@link module:engine/view/document~Document} and is available by default.
+ * Note that this observer is attached by the {@link module:engine/view/view~View} and is available by default.
*
* @see module:engine/view/observer/mutationobserver~MutationObserver
* @extends module:engine/view/observer/observer~Observer
@@ -51,7 +51,7 @@ export default class SelectionObserver extends Observer {
/* eslint-disable max-len */
/**
- * Reference to the {@link module:engine/view/document~Document#domConverter}.
+ * Reference to the {@link module:engine/view/view~View#domConverter}.
*
* @readonly
* @member {module:engine/view/domconverter~DomConverter} module:engine/view/observer/selectionobserver~SelectionObserver#domConverter
@@ -200,8 +200,7 @@ export default class SelectionObserver extends Observer {
* Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
*
* Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/selectionobserver~SelectionObserver
* @event module:engine/view/document~Document#event:selectionChange
@@ -218,8 +217,7 @@ export default class SelectionObserver extends Observer {
* Introduced by {@link module:engine/view/observer/selectionobserver~SelectionObserver}.
*
* Note that because {@link module:engine/view/observer/selectionobserver~SelectionObserver} is attached by the
- * {@link module:engine/view/document~Document}
- * this event is available by default.
+ * {@link module:engine/view/view~View} this event is available by default.
*
* @see module:engine/view/observer/selectionobserver~SelectionObserver
* @event module:engine/view/document~Document#event:selectionChangeDone
diff --git a/src/view/renderer.js b/src/view/renderer.js
index fc625e1f0..9f353749a 100644
--- a/src/view/renderer.js
+++ b/src/view/renderer.js
@@ -710,6 +710,13 @@ export default class Renderer {
}
}
}
+
+ /**
+ * Fired when {@link #render render} method is called. Actual rendering is executed as a listener to
+ * this event with default priority. This way other listeners can be used to run code before or after rendering.
+ *
+ * @event render
+ */
}
mix( Renderer, ObservableMixin );
diff --git a/src/view/view.js b/src/view/view.js
index d377ddf57..6913e776d 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -4,7 +4,7 @@
*/
/**
- * @module engine/view/document
+ * @module engine/view/view
*/
import Document from './document';
@@ -25,6 +25,28 @@ import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/sc
import { injectUiElementHandling } from './uielement';
import { injectQuirksHandling } from './filler';
+/**
+ * Editor's view controller class.
+ * It combines the actual tree of view elements - {@link module:engine/view/document~Document}, tree of DOM elements,
+ * {@link module:engine/view/domconverter~DomConverter DOM Converter}, {@link module:engine/view/renderer~Renderer renderer} and all
+ * {@link module:engine/view/observer/observer~Observer observers}.
+ *
+ * To modify view nodes use {@link module:engine/view/writer~Writer view writer}, which can be
+ * accessed by using {@link module:engine/view/view~View#change} method.
+ *
+ * If you want to only transform the tree of view elements to the DOM elements you can use the
+ * {@link module:engine/view/domconverter~DomConverter DomConverter}.
+ *
+ * Note that the following observers are added by the class constructor and are always available:
+ *
+ * * {@link module:engine/view/observer/mutationobserver~MutationObserver},
+ * * {@link module:engine/view/observer/selectionobserver~SelectionObserver},
+ * * {@link module:engine/view/observer/focusobserver~FocusObserver},
+ * * {@link module:engine/view/observer/keyobserver~KeyObserver},
+ * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}.
+ *
+ * @mixes module:utils/observablemixin~ObservableMixin
+ */
export default class View {
constructor() {
this.document = new Document();
@@ -37,6 +59,7 @@ export default class View {
// TODO: observers docs fixes
// TODO: check where writer instance is created and it should be returned by change() method only (converters!)
// TODO: manual tests
+ // TODO: placeholder - use change() block
/**
* Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by
diff --git a/src/view/writer.js b/src/view/writer.js
index c384308c3..c911f7344 100644
--- a/src/view/writer.js
+++ b/src/view/writer.js
@@ -18,6 +18,11 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import DocumentFragment from './documentfragment';
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
+/**
+ * View writer class. Provides set of methods used to properly manipulate nodes attached to
+ * {@link module:engine/view/document~Document view document}. To get an instance of view writer associated with
+ * the document use {@link module:engine/view/view~View#change view.change()) method.
+ */
export default class Writer {
/**
* Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
@@ -32,8 +37,8 @@ export default class Writer {
*
* **Note:** {@link module:engine/view/documentfragment~DocumentFragment DocumentFragment} is treated like a container.
*
- * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
- * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
+ * **Note:** Difference between {@link module:engine/view/writer~Writer#breakAttributes breakAttributes} and
+ * {@link module:engine/view/writer~Writer#breakContainer breakContainer} is that `breakAttributes` breaks all
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
* up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
* `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
@@ -52,8 +57,7 @@ export default class Writer {
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.breakContainer
- * @function module:engine/view/writer~writer.breakAttributes
+ * @see module:engine/view/writer~Writer#breakContainer
* @param {module:engine/view/position~Position|module:engine/view/range~Range} positionOrRange Position where
* to break attribute elements.
* @returns {module:engine/view/position~Position|module:engine/view/range~Range} New position or range, after breaking the attribute
@@ -77,16 +81,15 @@ export default class Writer {
*
^foobar
-> ^
foobar
*
foobar^
->
foobar
^
*
- * **Note:** Difference between {@link module:engine/view/writer~writer.breakAttributes breakAttributes} and
- * {@link module:engine/view/writer~writer.breakContainer breakContainer} is that `breakAttributes` breaks all
+ * **Note:** Difference between {@link module:engine/view/writer~Writer#breakAttributes breakAttributes} and
+ * {@link module:engine/view/writer~Writer#breakContainer breakContainer} is that `breakAttributes` breaks all
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} that are ancestors of given `position`,
* up to the first encountered {@link module:engine/view/containerelement~ContainerElement container element}.
* `breakContainer` assumes that given `position` is directly in container element and breaks that container element.
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.breakAttributes
- * @function module:engine/view/writer~writer.breakContainer
+ * @see module:engine/view/writer~Writer#breakAttributes
* @param {module:engine/view/position~Position} position Position where to break element.
* @returns {module:engine/view/position~Position} Position between broken elements. If element has not been broken,
* the returned position is placed either before it or after it.
@@ -145,15 +148,14 @@ export default class Writer {
*
[]
->
[]
*
foo[]bar
->
foo{}bar
*
- * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
- * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
+ * **Note:** Difference between {@link module:engine/view/writer~Writer#mergeAttributes mergeAttributes} and
+ * {@link module:engine/view/writer~Writer#mergeContainers mergeContainers} is that `mergeAttributes` merges two
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.mergeContainers
- * @function module:engine/view/writer~writer.mergeAttributes
+ * @see module:engine/view/writer~Writer#mergeContainers
* @param {module:engine/view/position~Position} position Merge position.
* @returns {module:engine/view/position~Position} Position after merge.
*/
@@ -209,15 +211,14 @@ export default class Writer {
*
foo
^
bar
->
foo^bar
*
foo
^
bar
->
foo^bar
*
- * **Note:** Difference between {@link module:engine/view/writer~writer.mergeAttributes mergeAttributes} and
- * {@link module:engine/view/writer~writer.mergeContainers mergeContainers} is that `mergeAttributes` merges two
+ * **Note:** Difference between {@link module:engine/view/writer~Writer#mergeAttributes mergeAttributes} and
+ * {@link module:engine/view/writer~Writer#mergeContainers mergeContainers} is that `mergeAttributes` merges two
* {@link module:engine/view/attributeelement~AttributeElement attribute elements} or {@link module:engine/view/text~Text text nodes}
* while `mergeContainer` merges two {@link module:engine/view/containerelement~ContainerElement container elements}.
*
* @see module:engine/view/attributeelement~AttributeElement
* @see module:engine/view/containerelement~ContainerElement
- * @see module:engine/view/writer~writer.mergeAttributes
- * @function module:engine/view/writer~writer.mergeContainers
+ * @see module:engine/view/writer~Writer#mergeAttributes
* @param {module:engine/view/position~Position} position Merge position.
* @returns {module:engine/view/position~Position} Position after merge.
*/
@@ -255,7 +256,6 @@ export default class Writer {
* {@link module:engine/view/emptyelement~EmptyElement EmptyElements} or
* {@link module:engine/view/uielement~UIElement UIElements}.
*
- * @function insert
* @param {module:engine/view/position~Position} position Insertion position.
* @param {module:engine/view/text~Text|module:engine/view/attributeelement~AttributeElement|
* module:engine/view/containerelement~ContainerElement|module:engine/view/emptyelement~EmptyElement|
@@ -309,7 +309,6 @@ export default class Writer {
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
* same parent container.
*
- * @function module:engine/view/writer~writer.remove
* @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
* to a collapsed range showing the new position.
* @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
@@ -347,7 +346,6 @@ export default class Writer {
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
* same parent container.
*
- * @function module:engine/view/writer~writer.clear
* @param {module:engine/view/range~Range} range Range to clear.
* @param {module:engine/view/element~Element} element Element to remove.
*/
@@ -407,7 +405,6 @@ export default class Writer {
* {@link module:engine/view/range~Range#start start} and {@link module:engine/view/range~Range#end end} positions are not placed inside
* same parent container.
*
- * @function module:engine/view/writer~writer.move
* @param {module:engine/view/range~Range} sourceRange Range containing nodes to move.
* @param {module:engine/view/position~Position} targetPosition Position to insert.
* @returns {module:engine/view/range~Range} Range in target container. Inserted nodes are placed between
@@ -447,7 +444,6 @@ export default class Writer {
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
* an instance of {module:engine/view/attributeelement~AttributeElement AttributeElement}.
*
- * @function module:engine/view/writer~writer.wrap
* @param {module:engine/view/range~Range} range Range to wrap.
* @param {module:engine/view/attributeelement~AttributeElement} attribute Attribute element to use as wrapper.
* @param {module:engine/view/selection~Selection} [viewSelection=null] View selection to change, required when
@@ -771,7 +767,7 @@ export default class Writer {
}
/**
- * Helper function for `view.writer.wrap`. Wraps position with provided attribute element.
+ * Helper function for {@link #wrap}. Wraps position with provided attribute element.
* This method will also merge newly added attribute element with its siblings whenever possible.
*
* Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-wrap-invalid-attribute` when passed attribute element is not
From 1aab5c89911f425632c58f9ced1888d80e148b93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Thu, 25 Jan 2018 16:19:33 +0100
Subject: [PATCH 23/89] Added creator methods to view writer.
---
src/view/writer.js | 119 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 116 insertions(+), 3 deletions(-)
diff --git a/src/view/writer.js b/src/view/writer.js
index c911f7344..593e381f1 100644
--- a/src/view/writer.js
+++ b/src/view/writer.js
@@ -12,18 +12,131 @@ import ContainerElement from './containerelement';
import AttributeElement from './attributeelement';
import EmptyElement from './emptyelement';
import UIElement from './uielement';
-import Text from './text';
import Range from './range';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import DocumentFragment from './documentfragment';
import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
+import Text from './text';
+import Element from './element';
+import EditableElement from './editableelement';
/**
* View writer class. Provides set of methods used to properly manipulate nodes attached to
- * {@link module:engine/view/document~Document view document}. To get an instance of view writer associated with
- * the document use {@link module:engine/view/view~View#change view.change()) method.
+ * {@link module:engine/view/document~Document view document}. It is not recommended to use it directly. To get an instance
+ * of view writer associated with the document use {@link module:engine/view/view~View#change view.change()) method.
*/
export default class Writer {
+ /**
+ * Creates a new {@link module:engine/view/text~Text text node}.
+ *
+ * writer.createText( 'foo' );
+ *
+ * @param {String} data Text data.
+ * @returns {module:engine/view/text~Text} Created text node.
+ */
+ createText( data ) {
+ return new Text( data );
+ }
+
+ /**
+ * Creates new {@link module:engine/view/element~Element element}.
+ *
+ * writer.createElement( 'paragraph' );
+ * writer.createElement( 'paragraph', { 'alignment': 'center' } );
+ *
+ * @param {String} name Name of the element.
+ * @param {Object} [attributes] Elements attributes.
+ * @returns {module:engine/view/element~Element} Created element.
+ */
+ createElement( name, attributes ) {
+ return new Element( name, attributes );
+ }
+
+ /**
+ * Creates new {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
+ *
+ * writer.createDocumentFragment();
+ *
+ * @returns {module:engine/view/documentfragment~DocumentFragment} Created document fragment.
+ */
+ createDocumentFragment() {
+ return new DocumentFragment();
+ }
+
+ /**
+ * Creates new {@link module:engine/view/attributeelement~AttributeElement}.
+ *
+ * writer.createAttributeElement( 'paragraph' );
+ * writer.createAttributeElement( 'paragraph', { 'alignment': 'center' } );
+ *
+ * @param {String} name Name of the element.
+ * @param {Object} [attributes] Elements attributes.
+ * @returns {module:engine/view/attributeelement~AttributeElement} Created element.
+ */
+ createAttributeElement( name, attributes ) {
+ return new AttributeElement( name, attributes );
+ }
+
+ /**
+ * Creates new {@link module:engine/view/containerelement~ContainerElement}.
+ *
+ * writer.createContainerElement( 'paragraph' );
+ * writer.createContainerElement( 'paragraph', { 'alignment': 'center' } );
+ *
+ * @param {String} name Name of the element.
+ * @param {Object} [attributes] Elements attributes.
+ * @returns {module:engine/view/containerelement~ContainerElement} Created element.
+ */
+ createContainerElement( name, attributes ) {
+ return new ContainerElement( name, attributes );
+ }
+
+ /**
+ * Creates new {@link module:engine/view/editableelement~EditableElement}.
+ *
+ * writer.createEditableElement( document, 'paragraph' );
+ * writer.createEditableElement( document, 'paragraph', { 'alignment': 'center' } );
+ *
+ * @param {module:engine/view/document~Document} document View document.
+ * @param {String} name Name of the element.
+ * @param {Object} [attributes] Elements attributes.
+ * @returns {module:engine/view/editableelement~EditableElement} Created element.
+ */
+ createEditableElement( document, name, attributes ) {
+ const editableElement = new EditableElement( name, attributes );
+ editableElement.document = document;
+
+ return editableElement;
+ }
+
+ /**
+ * Creates new {@link module:engine/view/emptyelement~EmptyElement}.
+ *
+ * writer.createEmptyElement( 'paragraph' );
+ * writer.createEmptyElement( 'paragraph', { 'alignment': 'center' } );
+ *
+ * @param {String} name Name of the element.
+ * @param {Object} [attributes] Elements attributes.
+ * @returns {module:engine/view/emptyelement~EmptyElement} Created element.
+ */
+ createEmptyElement( name, attributes ) {
+ return new EmptyElement( name, attributes );
+ }
+
+ /**
+ * Creates new {@link module:engine/view/uielement~UIElement}.
+ *
+ * writer.createUIElement( 'paragraph' );
+ * writer.createUIElement( 'paragraph', { 'alignment': 'center' } );
+ *
+ * @param {String} name Name of the element.
+ * @param {Object} [attributes] Elements attributes.
+ * @returns {module:engine/view/uielement~UIElement} Created element.
+ */
+ createUIElement( name, attributes ) {
+ return new UIElement( name, attributes );
+ }
+
/**
* Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside
* up to a container element.
From 1ceb15ffec27792f819291fe1333a6bac07ae3de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Szymon=20Kup=C5=9B?=
Date: Fri, 26 Jan 2018 11:57:37 +0100
Subject: [PATCH 24/89] Added view instance to writer. Fixed placeholder update
listener.
---
src/controller/datacontroller.js | 3 ++-
src/dev-utils/model.js | 3 ++-
src/view/placeholder.js | 18 ++++---------
src/view/renderer.js | 2 +-
src/view/view.js | 2 +-
src/view/writer.js | 8 ++++--
tests/conversion/advanced-converters.js | 18 +++++++------
tests/manual/placeholder.js | 9 ++++---
tests/view/placeholder.js | 35 +++++++++----------------
tests/view/renderer.js | 33 ++++++++++++++++-------
tests/view/writer/breakattributes.js | 3 ++-
tests/view/writer/breakcontainer.js | 3 ++-
tests/view/writer/clear.js | 3 ++-
tests/view/writer/insert.js | 3 ++-
tests/view/writer/mergeattributes.js | 3 ++-
tests/view/writer/mergecontainers.js | 3 ++-
tests/view/writer/move.js | 3 ++-
tests/view/writer/remove.js | 3 ++-
tests/view/writer/rename.js | 3 ++-
tests/view/writer/unwrap.js | 3 ++-
tests/view/writer/wrap.js | 3 ++-
21 files changed, 91 insertions(+), 73 deletions(-)
diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js
index fb97d6622..cc91a133a 100644
--- a/src/controller/datacontroller.js
+++ b/src/controller/datacontroller.js
@@ -19,6 +19,7 @@ import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
import { convertText, convertToModelFragment } from '../conversion/view-to-model-converters';
import ViewDocumentFragment from '../view/documentfragment';
+import ViewDocument from '../view/document';
import ViewWriter from '../view/writer';
import ModelRange from '../model/range';
@@ -158,7 +159,7 @@ export default class DataController {
const modelRange = ModelRange.createIn( modelElementOrFragment );
const viewDocumentFragment = new ViewDocumentFragment();
- const viewWriter = new ViewWriter();
+ const viewWriter = new ViewWriter( new ViewDocument() );
this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment );
this.modelToView.convertInsert( modelRange, viewWriter );
diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js
index b05a25040..93bbacaca 100644
--- a/src/dev-utils/model.js
+++ b/src/dev-utils/model.js
@@ -21,6 +21,7 @@ import ModelSelection from '../model/selection';
import ModelDocumentFragment from '../model/documentfragment';
import ViewWriter from '../view/writer';
+import ViewDocument from '../view/document';
import ViewConversionDispatcher from '../conversion/viewconversiondispatcher';
import ViewSelection from '../view/selection';
import ViewDocumentFragment from '../view/documentfragment';
@@ -213,7 +214,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
modelToView.on( 'selection', convertCollapsedSelection() );
// Convert model to view.
- const writer = new ViewWriter();
+ const writer = new ViewWriter( new ViewDocument() );
modelToView.convertInsert( range, writer );
// Convert model selection to view selection.
diff --git a/src/view/placeholder.js b/src/view/placeholder.js
index f6472e68e..dd34de0d3 100644
--- a/src/view/placeholder.js
+++ b/src/view/placeholder.js
@@ -9,7 +9,6 @@
import extend from '@ckeditor/ckeditor5-utils/src/lib/lodash/extend';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
-import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import '../../theme/placeholder.css';
const listener = {};
@@ -27,17 +26,8 @@ const documentPlaceholders = new WeakMap();
* @param {Function} [checkFunction] If provided it will be called before checking if placeholder should be displayed.
* If function returns `false` placeholder will not be showed.
*/
-export function attachPlaceholder( element, placeholderText, checkFunction ) {
- const document = element.document;
-
- if ( !document ) {
- /**
- * Provided element is not placed in any {@link module:engine/view/document~Document}.
- *
- * @error view-placeholder-element-is-detached
- */
- throw new CKEditorError( 'view-placeholder-element-is-detached: Provided element is not placed in document.' );
- }
+export function attachPlaceholder( view, element, placeholderText, checkFunction ) {
+ const document = view.document;
// Detach placeholder if was used before.
detachPlaceholder( element );
@@ -45,7 +35,9 @@ export function attachPlaceholder( element, placeholderText, checkFunction ) {
// Single listener per document.
if ( !documentPlaceholders.has( document ) ) {
documentPlaceholders.set( document, new Map() );
- listener.listenTo( document, 'change', () => updateAllPlaceholders( document ) );
+
+ // Attach listener just before rendering and update placeholders.
+ listener.listenTo( view.renderer, 'render', () => updateAllPlaceholders( document ), { priority: 'highest' } );
}
// Store text in element's data attribute.
diff --git a/src/view/renderer.js b/src/view/renderer.js
index 9f353749a..2ac415c57 100644
--- a/src/view/renderer.js
+++ b/src/view/renderer.js
@@ -110,7 +110,7 @@ export default class Renderer {
*/
this._fakeSelectionContainer = null;
- // TODO: document rend er event.
+ // TODO: document render event.
this.decorate( 'render' );
}
diff --git a/src/view/view.js b/src/view/view.js
index 6913e776d..f0faf5426 100644
--- a/src/view/view.js
+++ b/src/view/view.js
@@ -50,7 +50,7 @@ import { injectQuirksHandling } from './filler';
export default class View {
constructor() {
this.document = new Document();
- this._writer = new Writer();
+ this._writer = new Writer( this.document );
// TODO: check docs
// TODO: move change event description to this file.
diff --git a/src/view/writer.js b/src/view/writer.js
index 593e381f1..61304cbe3 100644
--- a/src/view/writer.js
+++ b/src/view/writer.js
@@ -26,6 +26,10 @@ import EditableElement from './editableelement';
* of view writer associated with the document use {@link module:engine/view/view~View#change view.change()) method.
*/
export default class Writer {
+ constructor( document ) {
+ this.document = document;
+ }
+
/**
* Creates a new {@link module:engine/view/text~Text text node}.
*
@@ -102,9 +106,9 @@ export default class Writer {
* @param {Object} [attributes] Elements attributes.
* @returns {module:engine/view/editableelement~EditableElement} Created element.
*/
- createEditableElement( document, name, attributes ) {
+ createEditableElement( name, attributes ) {
const editableElement = new EditableElement( name, attributes );
- editableElement.document = document;
+ editableElement.document = this.document;
return editableElement;
}
diff --git a/tests/conversion/advanced-converters.js b/tests/conversion/advanced-converters.js
index fffedec67..654787d2c 100644
--- a/tests/conversion/advanced-converters.js
+++ b/tests/conversion/advanced-converters.js
@@ -15,7 +15,6 @@ import ViewElement from '../../src/view/element';
import ViewContainerElement from '../../src/view/containerelement';
import ViewAttributeElement from '../../src/view/attributeelement';
import ViewText from '../../src/view/text';
-import ViewWriter from '../../src/view/writer';
import ViewPosition from '../../src/view/position';
import ViewRange from '../../src/view/range';
@@ -31,17 +30,17 @@ import {
import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters';
describe( 'advanced-converters', () => {
- let model, modelDoc, modelRoot, viewWriter, viewRoot, modelDispatcher, viewDispatcher;
+ let model, modelDoc, modelRoot, view, viewRoot, modelDispatcher, viewDispatcher;
beforeEach( () => {
model = new Model();
modelDoc = model.document;
modelRoot = modelDoc.createRoot();
- viewWriter = new ViewWriter();
const editing = new EditingController( model );
- viewRoot = editing.view.document.getRoot();
+ view = editing.view;
+ viewRoot = view.document.getRoot();
// Set name of view root the same as dom root.
// This is a mock of attaching view root to dom root.
@@ -180,7 +179,7 @@ describe( 'advanced-converters', () => {
const viewElement = new ViewContainerElement( 'blockquote' );
conversionApi.mapper.bindElements( data.item, viewElement );
- viewWriter.insert( viewPosition, viewElement );
+ conversionApi.writer.insert( viewPosition, viewElement );
}, { priority: 'high' } );
modelDispatcher.on( 'attribute:linkHref:quote', linkHrefOnQuoteConverter, { priority: 'high' } );
@@ -197,7 +196,7 @@ describe( 'advanced-converters', () => {
// Attribute was removed -> remove the view link.
const viewLink = viewQuote.getChild( viewQuote.childCount - 1 );
- viewWriter.remove( ViewRange.createOn( viewLink ) );
+ conversionApi.writer.remove( ViewRange.createOn( viewLink ) );
consumable.consume( data.item, 'attribute:linkTitle' );
} else if ( data.attributeOldValue === null ) {
@@ -210,7 +209,7 @@ describe( 'advanced-converters', () => {
viewLink.setAttribute( 'title', data.item.getAttribute( 'linkTitle' ) );
}
- viewWriter.insert( new ViewPosition( viewQuote, viewQuote.childCount ), viewLink );
+ conversionApi.writer.insert( new ViewPosition( viewQuote, viewQuote.childCount ), viewLink );
} else {
// Attribute has changed -> change the existing view link.
const viewLink = viewQuote.getChild( viewQuote.childCount - 1 );
@@ -513,7 +512,10 @@ describe( 'advanced-converters', () => {
] );
modelRoot.appendChildren( modelElement );
- modelDispatcher.convertInsert( ModelRange.createIn( modelRoot ), viewWriter );
+
+ view.change( writer => {
+ modelDispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer );
+ } );
expect( viewToString( viewRoot ) ).to.equal(
'