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, '
foo bar
' ); - viewDocument.render(); + view.render(); viewDocument.on( 'selectionChange', selectionChangeSpy ); - viewDocument.on( 'render', renderSpy, { priority: 'low' } ); + view.renderer.on( 'render', renderSpy, { priority: 'low' } ); - viewDocument.on( 'render', () => { + view.renderer.on( 'render', () => { sinon.assert.callOrder( selectionChangeSpy, renderSpy ); done(); }, { priority: 'low' } ); @@ -183,13 +185,13 @@ describe( 'FocusObserver', () => { const renderSpy = sinon.spy(); setData( viewDocument, '
foo bar
' ); - viewDocument.render(); + view.render(); const domEditable = domRoot.childNodes[ 0 ]; viewDocument.on( 'selectionChange', selectionChangeSpy ); - viewDocument.on( 'render', renderSpy, { priority: 'low' } ); + view.renderer.on( 'render', renderSpy, { priority: 'low' } ); - viewDocument.on( 'render', () => { + view.renderer.on( 'render', () => { sinon.assert.notCalled( selectionChangeSpy ); sinon.assert.called( renderSpy ); diff --git a/tests/view/observer/keyobserver.js b/tests/view/observer/keyobserver.js index 5efb4ecc1..284cbe053 100644 --- a/tests/view/observer/keyobserver.js +++ b/tests/view/observer/keyobserver.js @@ -6,19 +6,20 @@ /* globals document */ import KeyObserver from '../../../src/view/observer/keyobserver'; -import ViewDocument from '../../../src/view/document'; +import View from '../../../src/view/view'; import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard'; describe( 'KeyObserver', () => { - let viewDocument, observer; + let view, viewDocument, observer; beforeEach( () => { - viewDocument = new ViewDocument(); - observer = viewDocument.getObserver( KeyObserver ); + view = new View(); + viewDocument = view.document; + observer = view.getObserver( KeyObserver ); } ); afterEach( () => { - viewDocument.destroy(); + view.destroy(); } ); it( 'should define domEventType', () => { diff --git a/tests/view/observer/mouseobserver.js b/tests/view/observer/mouseobserver.js index ec265c2c4..9392a5a3c 100644 --- a/tests/view/observer/mouseobserver.js +++ b/tests/view/observer/mouseobserver.js @@ -6,18 +6,19 @@ /* globals document */ import MouseObserver from '../../../src/view/observer/mouseobserver'; -import ViewDocument from '../../../src/view/document'; +import View from '../../../src/view/view'; describe( 'MouseObserver', () => { - let viewDocument, observer; + let view, viewDocument, observer; beforeEach( () => { - viewDocument = new ViewDocument(); - observer = viewDocument.addObserver( MouseObserver ); + view = new View(); + viewDocument = view.document; + observer = view.addObserver( MouseObserver ); } ); afterEach( () => { - viewDocument.destroy(); + view.destroy(); } ); it( 'should define domEventType', () => { diff --git a/tests/view/observer/mutationobserver.js b/tests/view/observer/mutationobserver.js index fffa58986..96343e4e1 100644 --- a/tests/view/observer/mutationobserver.js +++ b/tests/view/observer/mutationobserver.js @@ -5,30 +5,31 @@ /* globals document */ -import ViewDocument from '../../../src/view/document'; +import View from '../../../src/view/view'; import MutationObserver from '../../../src/view/observer/mutationobserver'; import UIElement from '../../../src/view/uielement'; import createViewRoot from '../_utils/createroot'; import { parse } from '../../../src/dev-utils/view'; describe( 'MutationObserver', () => { - let domEditor, viewDocument, viewRoot, mutationObserver, lastMutations, domRoot; + let view, domEditor, viewDocument, viewRoot, mutationObserver, lastMutations, domRoot; beforeEach( () => { domRoot = document.createElement( 'div' ); domRoot.innerHTML = '
'; document.body.appendChild( domRoot ); - viewDocument = new ViewDocument(); + view = new View(); + viewDocument = view.document; domEditor = document.getElementById( 'main' ); lastMutations = null; createViewRoot( viewDocument ); - viewDocument.attachDomRoot( domEditor ); + view.attachDomRoot( domEditor ); viewDocument.selection.removeAllRanges(); document.getSelection().removeAllRanges(); - mutationObserver = viewDocument.getObserver( MutationObserver ); + mutationObserver = view.getObserver( MutationObserver ); viewDocument.on( 'mutations', ( evt, mutations ) => { lastMutations = mutations; @@ -38,14 +39,14 @@ describe( 'MutationObserver', () => { viewRoot.appendChildren( parse( 'foobar' ) ); - viewDocument.render(); + view.render(); } ); afterEach( () => { mutationObserver.disable(); domRoot.parentElement.removeChild( domRoot ); - viewDocument.destroy(); + view.destroy(); } ); it( 'should handle typing', () => { @@ -65,7 +66,7 @@ describe( 'MutationObserver', () => { const additional = document.getElementById( 'additional' ); mutationObserver.disable(); createViewRoot( viewDocument, 'div', 'additional' ); - viewDocument.attachDomRoot( additional, 'additional' ); + view.attachDomRoot( additional, 'additional' ); additional.textContent = 'foobar'; mutationObserver.flush(); @@ -97,7 +98,7 @@ describe( 'MutationObserver', () => { it( 'should handle unbold', () => { viewRoot.removeChildren( 0, viewRoot.childCount ); viewRoot.appendChildren( parse( 'foo' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 0 ]; const domB = domP.childNodes[ 0 ]; @@ -200,13 +201,13 @@ describe( 'MutationObserver', () => { // Prepare AdditionalEditor createViewRoot( viewDocument, 'div', 'additional' ); - viewDocument.attachDomRoot( domAdditionalEditor, 'additional' ); + view.attachDomRoot( domAdditionalEditor, 'additional' ); viewDocument.getRoot( 'additional' ).appendChildren( parse( 'foobar' ) ); // Render AdditionalEditor (first editor has been rendered in the beforeEach function) - viewDocument.render(); + view.render(); domEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom'; domAdditionalEditor.childNodes[ 0 ].childNodes[ 0 ].data = 'foom'; @@ -224,12 +225,12 @@ describe( 'MutationObserver', () => { } ); it( 'should fire children mutation if the mutation occurred in the inline filler', () => { - const { view, selection } = parse( 'foo[]bar' ); + const { view: viewContainer, selection } = parse( 'foo[]bar' ); - viewRoot.appendChildren( view ); + viewRoot.appendChildren( viewContainer ); viewDocument.selection.setTo( selection ); - viewDocument.render(); + view.render(); const inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ]; inlineFiller.data += 'x'; @@ -242,19 +243,19 @@ describe( 'MutationObserver', () => { } ); it( 'should have no inline filler in mutation', () => { - const { view, selection } = parse( 'foo[]bar' ); + const { view: viewContainer, selection } = parse( 'foo[]bar' ); - viewRoot.appendChildren( view ); + viewRoot.appendChildren( viewContainer ); viewDocument.selection.setTo( selection ); - viewDocument.render(); + view.render(); let inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ]; inlineFiller.data += 'x'; - view.getChild( 1 ).appendChildren( parse( 'x' ) ); + viewContainer.getChild( 1 ).appendChildren( parse( 'x' ) ); mutationObserver.flush(); - viewDocument.render(); + view.render(); inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ]; inlineFiller.data += 'y'; @@ -270,7 +271,7 @@ describe( 'MutationObserver', () => { it( 'should have no block filler in mutation', () => { viewRoot.appendChildren( parse( '' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; domP.removeChild( domP.childNodes[ 0 ] ); @@ -289,7 +290,7 @@ describe( 'MutationObserver', () => { it( 'should ignore mutation with bogus br inserted on the end of the empty paragraph', () => { viewRoot.appendChildren( parse( '' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; domP.appendChild( document.createElement( 'br' ) ); @@ -302,7 +303,7 @@ describe( 'MutationObserver', () => { it( 'should ignore mutation with bogus br inserted on the end of the paragraph with text', () => { viewRoot.appendChildren( parse( 'foo' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; domP.appendChild( document.createElement( 'br' ) ); @@ -315,7 +316,7 @@ describe( 'MutationObserver', () => { it( 'should ignore mutation with bogus br inserted on the end of the paragraph while processing text mutations', () => { viewRoot.appendChildren( parse( 'foo' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; domP.childNodes[ 0 ].data = 'foo '; @@ -332,7 +333,7 @@ describe( 'MutationObserver', () => { it( 'should ignore child mutations which resulted in no changes – when element contains elements', () => { viewRoot.appendChildren( parse( '' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; const domY = document.createElement( 'y' ); @@ -366,7 +367,7 @@ describe( 'MutationObserver', () => { it( 'should not ignore mutation with br inserted not on the end of the paragraph', () => { viewRoot.appendChildren( parse( 'foo' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; domP.insertBefore( document.createElement( 'br' ), domP.childNodes[ 0 ] ); @@ -385,7 +386,7 @@ describe( 'MutationObserver', () => { it( 'should not ignore mutation inserting element different than br on the end of the empty paragraph', () => { viewRoot.appendChildren( parse( '' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; domP.appendChild( document.createElement( 'span' ) ); @@ -403,7 +404,7 @@ describe( 'MutationObserver', () => { it( 'should not ignore mutation inserting element different than br on the end of the paragraph with text', () => { viewRoot.appendChildren( parse( 'foo' ) ); - viewDocument.render(); + view.render(); const domP = domEditor.childNodes[ 2 ]; domP.appendChild( document.createElement( 'span' ) ); @@ -433,7 +434,7 @@ describe( 'MutationObserver', () => { const uiElement = new MyUIElement( 'div' ); viewRoot.appendChildren( uiElement ); - viewDocument.render(); + view.render(); } ); it( 'should not collect text mutations from UIElement', () => { diff --git a/tests/view/observer/observer.js b/tests/view/observer/observer.js index 04cb9b3d1..4760e0f2a 100644 --- a/tests/view/observer/observer.js +++ b/tests/view/observer/observer.js @@ -4,15 +4,16 @@ */ import Observer from '../../../src/view/observer/observer'; +import View from '../../../src/view/view'; describe( 'Observer', () => { describe( 'constructor()', () => { it( 'should create Observer with properties', () => { - const viewDocument = {}; - const observer = new Observer( viewDocument ); + const view = new View(); + const observer = new Observer( view ); expect( observer ).to.be.an.instanceof( Observer ); - expect( observer ).to.have.property( 'document' ).that.equals( viewDocument ); + expect( observer ).to.have.property( 'document' ).that.equals( view.document ); expect( observer ).to.have.property( 'isEnabled' ).that.is.false; } ); } ); diff --git a/tests/view/observer/selectionobserver.js b/tests/view/observer/selectionobserver.js index 40cde1a89..c28c7167b 100644 --- a/tests/view/observer/selectionobserver.js +++ b/tests/view/observer/selectionobserver.js @@ -8,7 +8,7 @@ import ViewRange from '../../../src/view/range'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import ViewSelection from '../../../src/view/selection'; -import ViewDocument from '../../../src/view/document'; +import View from '../../../src/view/view'; import SelectionObserver from '../../../src/view/observer/selectionobserver'; import FocusObserver from '../../../src/view/observer/focusobserver'; import log from '@ckeditor/ckeditor5-utils/src/log'; @@ -18,7 +18,7 @@ import { parse } from '../../../src/dev-utils/view'; testUtils.createSinonSandbox(); describe( 'SelectionObserver', () => { - let viewDocument, viewRoot, selectionObserver, domRoot, domMain, domDocument; + let view, viewDocument, viewRoot, selectionObserver, domRoot, domMain, domDocument; beforeEach( done => { domDocument = document; @@ -27,11 +27,12 @@ describe( 'SelectionObserver', () => { domMain = domRoot.childNodes[ 0 ]; domDocument.body.appendChild( domRoot ); - viewDocument = new ViewDocument(); + view = new View(); + viewDocument = view.document; createViewRoot( viewDocument ); - viewDocument.attachDomRoot( domMain ); + view.attachDomRoot( domMain ); - selectionObserver = viewDocument.getObserver( SelectionObserver ); + selectionObserver = view.getObserver( SelectionObserver ); viewRoot = viewDocument.getRoot(); @@ -39,7 +40,7 @@ describe( 'SelectionObserver', () => { 'xxx' + 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy' ) ); - viewDocument.render(); + view.render(); viewDocument.selection.removeAllRanges(); domDocument.getSelection().removeAllRanges(); @@ -56,7 +57,7 @@ describe( 'SelectionObserver', () => { afterEach( () => { domRoot.parentElement.removeChild( domRoot ); - viewDocument.destroy(); + view.destroy(); } ); it( 'should fire selectionChange when it is the only change', done => { @@ -86,7 +87,7 @@ describe( 'SelectionObserver', () => { it( 'should add only one listener to one document', done => { // Add second roots to ensure that listener is added once. createViewRoot( viewDocument, 'div', 'additional' ); - viewDocument.attachDomRoot( domDocument.getElementById( 'additional' ), 'additional' ); + view.attachDomRoot( domDocument.getElementById( 'additional' ), 'additional' ); viewDocument.on( 'selectionChange', () => { done(); @@ -104,11 +105,11 @@ describe( 'SelectionObserver', () => { const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 ); viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) ); - viewDocument.render(); + view.render(); } ); it( 'should not fired if observer is disabled', done => { - viewDocument.getObserver( SelectionObserver ).disable(); + view.getObserver( SelectionObserver ).disable(); viewDocument.on( 'selectionChange', () => { throw 'selectionChange on render'; @@ -208,8 +209,8 @@ describe( 'SelectionObserver', () => { // We need to recreate SelectionObserver, so it will use mocked setInterval. selectionObserver.disable(); selectionObserver.destroy(); - viewDocument._observers.delete( SelectionObserver ); - viewDocument.addObserver( SelectionObserver ); + view._observers.delete( SelectionObserver ); + view.addObserver( SelectionObserver ); return doChanges() .then( doChanges ) @@ -238,7 +239,7 @@ describe( 'SelectionObserver', () => { viewDocument.on( 'selectionChangeDone', spy ); // Disable focus observer to not re-render view on each focus. - viewDocument.getObserver( FocusObserver ).disable(); + view.getObserver( FocusObserver ).disable(); // Change selection. changeDomSelection(); @@ -316,13 +317,13 @@ describe( 'SelectionObserver', () => { // Normally this is handled by view -> model -> view selection converters chain. const viewSel = viewDocument.selection; - const viewAnchor = viewDocument.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset ); - const viewFocus = viewDocument.domConverter.domPositionToView( sel.focusNode, sel.focusOffset ); + const viewAnchor = view.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset ); + const viewFocus = view.domConverter.domPositionToView( sel.focusNode, sel.focusOffset ); viewSel.setCollapsedAt( viewAnchor ); viewSel.moveFocusTo( viewFocus ); - viewDocument.render(); + view.render(); } ); viewDocument.once( 'selectionChange', () => { @@ -330,7 +331,7 @@ describe( 'SelectionObserver', () => { selectionObserver.listenTo( domDocument, 'selectionchange', () => { // 4. Check if view was re-rendered. - expect( viewDocument.render.called ).to.be.true; + expect( view.render.called ).to.be.true; done(); }, { priority: 'lowest' } ); @@ -339,7 +340,7 @@ describe( 'SelectionObserver', () => { // Current and new selection position are similar in view (but not equal!). // Also add a spy to `viewDocument#render` to see if view will be re-rendered. sel.collapse( domUI, 0 ); - sinon.spy( viewDocument, 'render' ); + sinon.spy( view, 'render' ); // Some browsers like Safari won't allow to put selection inside empty ui element. // In that situation selection should stay in correct place. diff --git a/tests/view/renderer.js b/tests/view/renderer.js index 004d9fc3a..65bcfe925 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -5,7 +5,7 @@ /* globals document, window, NodeFilter */ -import ViewDocument from '../../src/view/document'; +import View from '../../src/view/view'; import ViewElement from '../../src/view/element'; import ViewContainerElement from '../../src/view/containerelement'; import ViewAttributeElement from '../../src/view/attributeelement'; @@ -21,15 +21,16 @@ import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, BR_FILLER } from '. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import createViewRoot from './_utils/createroot'; import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; -import { unwrap, insert, remove } from '../../src/view/writer'; +import Writer from '../../src/view/writer'; import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml'; testUtils.createSinonSandbox(); describe( 'Renderer', () => { - let selection, domConverter, renderer; + let selection, domConverter, renderer, writer; beforeEach( () => { + writer = new Writer(); selection = new Selection(); domConverter = new DomConverter(); renderer = new Renderer( domConverter, selection ); @@ -1875,15 +1876,16 @@ describe( 'Renderer', () => { } ); describe( '#922', () => { - let viewDoc, viewRoot, domRoot, converter; + let view, viewDoc, viewRoot, domRoot, converter; beforeEach( () => { - viewDoc = new ViewDocument(); + view = new View(); + viewDoc = view.document; domRoot = document.createElement( 'div' ); document.body.appendChild( domRoot ); viewRoot = createViewRoot( viewDoc ); - viewDoc.attachDomRoot( domRoot ); - converter = viewDoc.domConverter; + view.attachDomRoot( domRoot ); + converter = view.domConverter; } ); it( 'should properly render unwrapped attributes #1', () => { @@ -1897,14 +1899,14 @@ describe( 'Renderer', () => { ); // Render it to DOM to create initial DOM <-> view mappings. - viewDoc.render(); + view.render(); // Unwrap italic attribute element. - unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); + writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); expect( getViewData( viewDoc ) ).to.equal( '

[foo]

' ); // Re-render changes in view to DOM. - viewDoc.render(); + view.render(); // Check if DOM is rendered correctly. expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '

foo

' ); @@ -1920,15 +1922,15 @@ describe( 'Renderer', () => { '' ); // Render it to DOM to create initial DOM <-> view mappings. - viewDoc.render(); + view.render(); // Unwrap italic attribute element and change text inside. - unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); + writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); viewRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ).data = 'bar'; expect( getViewData( viewDoc ) ).to.equal( '

[bar]

' ); // Re-render changes in view to DOM. - viewDoc.render(); + view.render(); // Check if DOM is rendered correctly. expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '

bar

' ); @@ -1941,16 +1943,16 @@ describe( 'Renderer', () => { ); // Render it to DOM to create initial DOM <-> view mappings. - viewDoc.render(); + view.render(); // Change text and insert new element into paragraph. const textNode = viewRoot.getChild( 0 ).getChild( 0 ); textNode.data = 'foobar'; - insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) ); + writer.insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) ); expect( getViewData( viewDoc ) ).to.equal( '

foobar

' ); // Re-render changes in view to DOM. - viewDoc.render(); + view.render(); // Check if DOM is rendered correctly. expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '

foobar

' ); @@ -1963,16 +1965,16 @@ describe( 'Renderer', () => { ); // Render it to DOM to create initial DOM <-> view mappings. - viewDoc.render(); + view.render(); // Change text and insert new element into paragraph. const textNode = viewRoot.getChild( 0 ).getChild( 0 ); textNode.data = 'foobar'; - insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) ); + writer.insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) ); expect( getViewData( viewDoc ) ).to.equal( '

foobar

' ); // Re-render changes in view to DOM. - viewDoc.render(); + view.render(); // Check if DOM is rendered correctly. expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '

foobar

' ); @@ -1989,18 +1991,18 @@ describe( 'Renderer', () => { ); // Render it to DOM to create initial DOM <-> view mappings. - viewDoc.render(); + view.render(); // Remove first element and reinsert it at the end. const container = viewRoot.getChild( 0 ); const firstElement = container.getChild( 0 ); - remove( ViewRange.createOn( firstElement ) ); - insert( new ViewPosition( container, 2 ), firstElement ); + writer.remove( ViewRange.createOn( firstElement ) ); + writer.insert( new ViewPosition( container, 2 ), firstElement ); expect( getViewData( viewDoc ) ).to.equal( '

' ); // Re-render changes in view to DOM. - viewDoc.render(); + view.render(); // Check if DOM is rendered correctly. expect( normalizeHtml( domRoot.innerHTML ) ).to.equal( '

' ); diff --git a/tests/view/document/jumpoverinlinefiller.js b/tests/view/view/jumpoverinlinefiller.js similarity index 87% rename from tests/view/document/jumpoverinlinefiller.js rename to tests/view/view/jumpoverinlinefiller.js index a64d9f2ad..f22c9fba4 100644 --- a/tests/view/document/jumpoverinlinefiller.js +++ b/tests/view/view/jumpoverinlinefiller.js @@ -6,7 +6,7 @@ /* globals document */ import ViewRange from '../../../src/view/range'; -import ViewDocument from '../../../src/view/document'; +import View from '../../../src/view/view'; import { INLINE_FILLER_LENGTH, isInlineFiller, startsWithFiller } from '../../../src/view/filler'; import createViewRoot from '../_utils/createroot'; @@ -15,8 +15,8 @@ import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; import { parse, setData } from '../../../src/dev-utils/view'; -describe( 'Document', () => { - let viewDocument, domRoot; +describe( 'View', () => { + let view, viewDocument, domRoot; beforeEach( () => { domRoot = createElement( document, 'div', { @@ -24,17 +24,18 @@ describe( 'Document', () => { } ); document.body.appendChild( domRoot ); - viewDocument = new ViewDocument(); + view = new View(); + viewDocument = view.document; createViewRoot( viewDocument ); - viewDocument.attachDomRoot( domRoot ); + view.attachDomRoot( domRoot ); document.getSelection().removeAllRanges(); - viewDocument.isFocused = true; + view.isFocused = true; } ); afterEach( () => { - viewDocument.destroy(); + view.destroy(); domRoot.parentElement.removeChild( domRoot ); } ); @@ -42,9 +43,9 @@ describe( 'Document', () => { describe( 'jump over inline filler hack', () => { it( 'should jump over inline filler when left arrow is pressed after inline filler', () => { setData( viewDocument, 'foo[]bar' ); - viewDocument.render(); + view.render(); - viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } ); + viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } ); const domSelection = document.getSelection(); @@ -64,9 +65,9 @@ describe( 'Document', () => { it( 'should do nothing when another key is pressed', () => { setData( viewDocument, 'foo[]bar' ); - viewDocument.render(); + view.render(); - viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) } ); + viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: view.domRoots.get( 'main' ) } ); const domSelection = document.getSelection(); @@ -77,9 +78,9 @@ describe( 'Document', () => { it( 'should do nothing if range is not collapsed', () => { setData( viewDocument, 'foo{x}bar' ); - viewDocument.render(); + view.render(); - viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } ); + viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } ); const domSelection = document.getSelection(); @@ -105,7 +106,7 @@ describe( 'Document', () => { it( 'should do nothing if caret is not directly before the filler', () => { setData( viewDocument, 'foo[]bar' ); - viewDocument.render(); + view.render(); // Insert a letter to the : 'foox{}bar' // Do this both in the view and in the DOM to simulate typing and to avoid rendering (which would remove the filler). @@ -115,7 +116,7 @@ describe( 'Document', () => { viewDocument.selection.removeAllRanges(); viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) ); - const domB = viewDocument.getDomRoot( 'main' ).querySelector( 'b' ); + const domB = view.getDomRoot( 'main' ).querySelector( 'b' ); const domSelection = document.getSelection(); domB.childNodes[ 0 ].data += 'x'; @@ -125,9 +126,9 @@ describe( 'Document', () => { domRange.collapse( true ); domSelection.addRange( domRange ); - viewDocument.render(); + view.render(); - viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: viewDocument.domRoots.get( 'main' ) } ); + viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } ); expect( startsWithFiller( domSelection.anchorNode ) ).to.be.true; expect( domSelection.anchorOffset ).to.equal( INLINE_FILLER_LENGTH + 1 ); diff --git a/tests/view/document/jumpoveruielement.js b/tests/view/view/jumpoveruielement.js similarity index 96% rename from tests/view/document/jumpoveruielement.js rename to tests/view/view/jumpoveruielement.js index fdea883e2..625e7bb4d 100644 --- a/tests/view/document/jumpoveruielement.js +++ b/tests/view/view/jumpoveruielement.js @@ -5,7 +5,7 @@ /* globals document */ -import ViewDocument from '../../../src/view/document'; +import View from '../../../src/view/view'; import UIElement from '../../../src/view/uielement'; import ViewContainerElement from '../../../src/view/containerelement'; import ViewAttribtueElement from '../../../src/view/attributeelement'; @@ -18,7 +18,7 @@ import { setData as setViewData } from '../../../src/dev-utils/view'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; describe( 'Document', () => { - let viewDocument, domRoot, domSelection, viewRoot, foo, bar, ui, ui2; + let view, viewDocument, domRoot, domSelection, viewRoot, foo, bar, ui, ui2; class MyUIElement extends UIElement { render( domDocument ) { @@ -35,14 +35,15 @@ describe( 'Document', () => { } ); document.body.appendChild( domRoot ); - viewDocument = new ViewDocument(); + view = new View(); + viewDocument = view.document; viewRoot = createViewRoot( viewDocument ); - viewDocument.attachDomRoot( domRoot ); + view.attachDomRoot( domRoot ); domSelection = document.getSelection(); domSelection.removeAllRanges(); - viewDocument.isFocused = true; + view.isFocused = true; foo = new ViewText( 'foo' ); bar = new ViewText( 'bar' ); @@ -54,15 +55,15 @@ describe( 'Document', () => { } ); afterEach( () => { - viewDocument.destroy(); + view.destroy(); domRoot.parentElement.removeChild( domRoot ); } ); function renderAndFireKeydownEvent( options ) { - viewDocument.render(); + view.render(); - const eventData = Object.assign( { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) }, options ); + const eventData = Object.assign( { keyCode: keyCodes.arrowright, domTarget: view.domRoots.get( 'main' ) }, options ); viewDocument.fire( 'keydown', eventData ); } @@ -416,14 +417,14 @@ describe( 'Document', () => { } ); } ); - it( 'should do nothing if dom position cannot be converted to view position', () => { + it( 'should do nothing if DOM position cannot be converted to view position', () => { const newDiv = document.createElement( 'div' ); const domSelection = document.getSelection(); document.body.appendChild( newDiv ); domSelection.collapse( newDiv, 0 ); - viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: viewDocument.domRoots.get( 'main' ) } ); + viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowright, domTarget: view.domRoots.get( 'main' ) } ); } ); } ); } ); diff --git a/tests/view/view.js b/tests/view/view/view.js similarity index 55% rename from tests/view/view.js rename to tests/view/view/view.js index cb73d3c0d..c9ee1248d 100644 --- a/tests/view/view.js +++ b/tests/view/view/view.js @@ -1,25 +1,29 @@ /* globals document */ -import View from '../../src/view/view'; -import MutationObserver from '../../src/view/observer/mutationobserver'; +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 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 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 ViewRange from '../../../src/view/range'; +import RootEditableElement from '../../../src/view/rooteditableelement'; +import ViewElement from '../../../src/view/element'; +import { isBlockFiller, BR_FILLER } from '../../../src/view/filler'; import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; describe( 'view', () => { const DEFAULT_OBSERVERS_COUNT = 5; let domRoot, view, viewDocument, ObserverMock, instantiated, enabled, ObserverMockGlobalCount; + testUtils.createSinonSandbox(); + beforeEach( () => { domRoot = createElement( document, 'div', { id: 'editor', @@ -30,8 +34,8 @@ describe( 'view', () => { viewDocument = view.document; ObserverMock = class extends Observer { - constructor( viewDocument ) { - super( viewDocument ); + constructor( view ) { + super( view ); this.enable = sinon.spy(); this.disable = sinon.spy(); @@ -43,8 +47,8 @@ describe( 'view', () => { enabled = 0; ObserverMockGlobalCount = class extends Observer { - constructor( viewDocument ) { - super( viewDocument ); + constructor( view ) { + super( view ); instantiated++; this.observe = sinon.spy(); @@ -74,38 +78,39 @@ describe( 'view', () => { const domDiv = document.createElement( 'div' ); const viewRoot = createViewRoot( viewDocument, 'div', 'main' ); - expect( count( viewDocument.domRoots ) ).to.equal( 0 ); + expect( count( view.domRoots ) ).to.equal( 0 ); - viewDocument.attachDomRoot( domDiv ); + view.attachDomRoot( domDiv ); - expect( count( viewDocument.domRoots ) ).to.equal( 1 ); + expect( count( view.domRoots ) ).to.equal( 1 ); - expect( viewDocument.getDomRoot() ).to.equal( domDiv ); - expect( viewDocument.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv ); + expect( view.getDomRoot() ).to.equal( domDiv ); + expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv ); - expect( viewDocument.renderer.markedChildren.has( viewRoot ) ).to.be.true; + expect( view.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 ); + expect( count( view.domRoots ) ).to.equal( 0 ); - viewDocument.attachDomRoot( domH1, 'header' ); + view.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; + expect( count( view.domRoots ) ).to.equal( 1 ); + expect( view.getDomRoot( 'header' ) ).to.equal( domH1 ); + expect( view.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 ); + expect( view.renderer.markedChildren.has( viewH1 ) ).to.be.true; } ); it( 'should call observe on each observer', () => { // The variable will be overwritten. - viewDocument.destroy(); + view.destroy(); - viewDocument = new Document( document.createElement( 'div' ) ); - viewDocument.renderer.render = sinon.spy(); + view = new View(); + viewDocument = view.document; + view.renderer.render = sinon.spy(); const domDiv1 = document.createElement( 'div' ); domDiv1.setAttribute( 'id', 'editor' ); @@ -113,11 +118,11 @@ describe( 'view', () => { const domDiv2 = document.createElement( 'div' ); domDiv2.setAttribute( 'id', 'editor' ); - const observerMock = viewDocument.addObserver( ObserverMock ); - const observerMockGlobalCount = viewDocument.addObserver( ObserverMockGlobalCount ); + const observerMock = view.addObserver( ObserverMock ); + const observerMockGlobalCount = view.addObserver( ObserverMockGlobalCount ); createViewRoot( viewDocument, 'div', 'root1' ); - viewDocument.attachDomRoot( document.createElement( 'div' ), 'root1' ); + view.attachDomRoot( document.createElement( 'div' ), 'root1' ); sinon.assert.calledOnce( observerMock.observe ); sinon.assert.calledOnce( observerMockGlobalCount.observe ); @@ -127,28 +132,29 @@ describe( 'view', () => { describe( 'addObserver()', () => { beforeEach( () => { // The variable will be overwritten. - viewDocument.destroy(); + view.destroy(); - viewDocument = new Document( document.createElement( 'div' ) ); - viewDocument.renderer.render = sinon.spy(); + view = new View(); + viewDocument = view.document; + view.renderer.render = sinon.spy(); } ); afterEach( () => { - viewDocument.destroy(); + view.destroy(); } ); it( 'should be instantiated and enabled on adding', () => { - const observerMock = viewDocument.addObserver( ObserverMock ); + const observerMock = view.addObserver( ObserverMock ); - expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 ); + expect( view._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 ); + const observerMock1 = view.addObserver( ObserverMock ); + const observerMock2 = view.addObserver( ObserverMock ); expect( observerMock1 ).to.be.instanceof( ObserverMock ); expect( observerMock2 ).to.be.instanceof( ObserverMock ); @@ -156,15 +162,15 @@ describe( 'view', () => { } ); it( 'should instantiate one observer only once', () => { - viewDocument.addObserver( ObserverMockGlobalCount ); - viewDocument.addObserver( ObserverMockGlobalCount ); + view.addObserver( ObserverMockGlobalCount ); + view.addObserver( ObserverMockGlobalCount ); - expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 1 ); + expect( view._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 ); + view.addObserver( ObserverMock ); + expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 ); } ); it( 'should instantiate child class of already registered observer', () => { @@ -175,18 +181,18 @@ describe( 'view', () => { enable() {} } - viewDocument.addObserver( ObserverMock ); - viewDocument.addObserver( ChildObserverMock ); + view.addObserver( ObserverMock ); + view.addObserver( ChildObserverMock ); - expect( viewDocument._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 ); + expect( view._observers.size ).to.equal( DEFAULT_OBSERVERS_COUNT + 2 ); } ); it( 'should be disabled and re-enabled on render', () => { - const observerMock = viewDocument.addObserver( ObserverMock ); - viewDocument.render(); + const observerMock = view.addObserver( ObserverMock ); + view.render(); sinon.assert.calledOnce( observerMock.disable ); - sinon.assert.calledOnce( viewDocument.renderer.render ); + sinon.assert.calledOnce( view.renderer.render ); sinon.assert.calledTwice( observerMock.enable ); } ); @@ -194,10 +200,10 @@ describe( 'view', () => { createViewRoot( viewDocument, 'div', 'roo1' ); createViewRoot( viewDocument, 'div', 'roo2' ); - viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo1' ); - viewDocument.attachDomRoot( document.createElement( 'div' ), 'roo2' ); + view.attachDomRoot( document.createElement( 'div' ), 'roo1' ); + view.attachDomRoot( document.createElement( 'div' ), 'roo2' ); - const observerMock = viewDocument.addObserver( ObserverMock ); + const observerMock = view.addObserver( ObserverMock ); sinon.assert.calledTwice( observerMock.observe ); } ); @@ -205,15 +211,15 @@ describe( 'view', () => { describe( 'getObserver()', () => { it( 'should return observer it it is added', () => { - const addedObserverMock = viewDocument.addObserver( ObserverMock ); - const getObserverMock = viewDocument.getObserver( ObserverMock ); + const addedObserverMock = view.addObserver( ObserverMock ); + const getObserverMock = view.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 ); + const getObserverMock = view.getObserver( ObserverMock ); expect( getObserverMock ).to.be.undefined; } ); @@ -228,16 +234,16 @@ describe( 'view', () => { it( 'does nothing when there are no ranges in the selection', () => { const stub = testUtils.sinon.stub( global.window, 'scrollTo' ); - viewDocument.scrollToTheSelection(); + view.scrollToTheSelection(); sinon.assert.notCalled( stub ); } ); - it( 'scrolls to the first range in selection with an offset', () => { + it.only( '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 ); + view.attachDomRoot( domRoot ); // Make sure the window will have to scroll to the domRoot. Object.assign( domRoot.style, { @@ -248,19 +254,19 @@ describe( 'view', () => { viewDocument.selection.addRange( range ); - viewDocument.scrollToTheSelection(); + view.scrollToTheSelection(); sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number ); } ); } ); describe( 'disableObservers()', () => { it( 'should disable observers', () => { - const addedObserverMock = viewDocument.addObserver( ObserverMock ); + const addedObserverMock = view.addObserver( ObserverMock ); expect( addedObserverMock.enable.calledOnce ).to.be.true; expect( addedObserverMock.disable.called ).to.be.false; - viewDocument.disableObservers(); + view.disableObservers(); expect( addedObserverMock.enable.calledOnce ).to.be.true; expect( addedObserverMock.disable.calledOnce ).to.be.true; @@ -269,14 +275,14 @@ describe( 'view', () => { describe( 'enableObservers()', () => { it( 'should enable observers', () => { - const addedObserverMock = viewDocument.addObserver( ObserverMock ); + const addedObserverMock = view.addObserver( ObserverMock ); - viewDocument.disableObservers(); + view.disableObservers(); expect( addedObserverMock.enable.calledOnce ).to.be.true; expect( addedObserverMock.disable.calledOnce ).to.be.true; - viewDocument.enableObservers(); + view.enableObservers(); expect( addedObserverMock.enable.calledTwice ).to.be.true; expect( addedObserverMock.disable.calledOnce ).to.be.true; @@ -291,7 +297,7 @@ describe( 'view', () => { domEditable.setAttribute( 'contenteditable', 'true' ); document.body.appendChild( domEditable ); viewEditable = createViewRoot( viewDocument, 'div', 'main' ); - viewDocument.attachDomRoot( domEditable ); + view.attachDomRoot( domEditable ); viewDocument.selection.addRange( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) ); } ); @@ -300,10 +306,10 @@ describe( 'view', () => { } ); it( 'should focus editable with selection', () => { - const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' ); - const renderSpy = testUtils.sinon.spy( viewDocument, 'render' ); + const converterFocusSpy = testUtils.sinon.spy( view.domConverter, 'focus' ); + const renderSpy = testUtils.sinon.spy( view, 'render' ); - viewDocument.focus(); + view.focus(); expect( converterFocusSpy.called ).to.be.true; expect( renderSpy.calledOnce ).to.be.true; @@ -317,11 +323,11 @@ describe( 'view', () => { } ); it( 'should not focus if document is already focused', () => { - const converterFocusSpy = testUtils.sinon.spy( viewDocument.domConverter, 'focus' ); - const renderSpy = testUtils.sinon.spy( viewDocument, 'render' ); + const converterFocusSpy = testUtils.sinon.spy( view.domConverter, 'focus' ); + const renderSpy = testUtils.sinon.spy( view, 'render' ); viewDocument.isFocused = true; - viewDocument.focus(); + view.focus(); expect( converterFocusSpy.called ).to.be.false; expect( renderSpy.called ).to.be.false; @@ -331,7 +337,7 @@ describe( 'view', () => { const logSpy = testUtils.sinon.stub( log, 'warn' ); viewDocument.selection.removeAllRanges(); - viewDocument.focus(); + view.focus(); expect( logSpy.calledOnce ).to.be.true; expect( logSpy.args[ 0 ][ 0 ] ).to.match( /^view-focus-no-selection/ ); } ); @@ -340,33 +346,96 @@ describe( 'view', () => { describe( 'isFocused', () => { it( 'should change renderer.isFocused too', () => { expect( viewDocument.isFocused ).to.equal( false ); - expect( viewDocument.renderer.isFocused ).to.equal( false ); + expect( view.renderer.isFocused ).to.equal( false ); viewDocument.isFocused = true; expect( viewDocument.isFocused ).to.equal( true ); - expect( viewDocument.renderer.isFocused ).to.equal( true ); + expect( view.renderer.isFocused ).to.equal( true ); } ); } ); describe( 'render()', () => { - it( 'should fire an event', () => { - const spy = sinon.spy(); + it( 'disable observers, renders and enable observers', () => { + const observerMock = view.addObserver( ObserverMock ); + const renderStub = sinon.stub( view.renderer, 'render' ); + + view.render(); + + sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable ); + } ); + } ); - viewDocument.on( 'render', spy ); + describe( 'view and DOM integration', () => { + it( 'should remove content of the DOM', () => { + const domDiv = createElement( document, 'div', { id: 'editor' }, [ + createElement( document, 'p' ), + createElement( document, 'p' ) + ] ); - viewDocument.render(); + const view = new View(); + const viewDocument = view.document; - expect( spy.calledOnce ).to.be.true; + createRoot( 'div', 'main', viewDocument ); + view.attachDomRoot( domDiv ); + view.render(); + + expect( domDiv.childNodes.length ).to.equal( 1 ); + expect( isBlockFiller( domDiv.childNodes[ 0 ], BR_FILLER ) ).to.be.true; + + view.destroy(); } ); - it( 'disable observers, renders and enable observers', () => { - const observerMock = viewDocument.addObserver( ObserverMock ); - const renderStub = sinon.stub( viewDocument.renderer, 'render' ); + it( 'should render changes in the Document', () => { + const domDiv = document.createElement( 'div' ); - viewDocument.render(); + const view = new View(); + const viewDocument = view.document; + createRoot( 'div', 'main', viewDocument ); + view.attachDomRoot( domDiv ); - sinon.assert.callOrder( observerMock.disable, renderStub, observerMock.enable ); + viewDocument.getRoot().appendChildren( new ViewElement( 'p' ) ); + view.render(); + + expect( domDiv.childNodes.length ).to.equal( 1 ); + expect( domDiv.childNodes[ 0 ].tagName ).to.equal( 'P' ); + + view.destroy(); + } ); + + it( 'should render attribute changes', () => { + const domRoot = document.createElement( 'div' ); + + const view = new View(); + const viewDocument = view.document; + const viewRoot = createRoot( 'div', 'main', viewDocument ); + + view.attachDomRoot( domRoot ); + + const viewP = new ViewElement( 'p', { class: 'foo' } ); + viewRoot.appendChildren( viewP ); + view.render(); + + expect( domRoot.childNodes.length ).to.equal( 1 ); + expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' ); + + viewP.setAttribute( 'class', 'bar' ); + view.render(); + + expect( domRoot.childNodes.length ).to.equal( 1 ); + expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' ); + + view.destroy(); } ); } ); + + function createRoot( name, rootName, viewDoc ) { + const viewRoot = new RootEditableElement( name ); + + viewRoot.rootName = rootName; + viewRoot.document = viewDoc; + viewDoc.roots.add( viewRoot ); + + return viewRoot; + } } ); From 3735a668f840ed3cecb22ddbddc638a97a932bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 15 Jan 2018 16:39:16 +0100 Subject: [PATCH 09/89] Fixed view controller tests. --- tests/view/view/jumpoverinlinefiller.js | 2 +- tests/view/view/jumpoveruielement.js | 2 +- tests/view/view/view.js | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/view/view/jumpoverinlinefiller.js b/tests/view/view/jumpoverinlinefiller.js index f22c9fba4..d1653e39d 100644 --- a/tests/view/view/jumpoverinlinefiller.js +++ b/tests/view/view/jumpoverinlinefiller.js @@ -31,7 +31,7 @@ describe( 'View', () => { document.getSelection().removeAllRanges(); - view.isFocused = true; + viewDocument.isFocused = true; } ); afterEach( () => { diff --git a/tests/view/view/jumpoveruielement.js b/tests/view/view/jumpoveruielement.js index 625e7bb4d..03cac5e48 100644 --- a/tests/view/view/jumpoveruielement.js +++ b/tests/view/view/jumpoveruielement.js @@ -43,7 +43,7 @@ describe( 'Document', () => { domSelection = document.getSelection(); domSelection.removeAllRanges(); - view.isFocused = true; + viewDocument.isFocused = true; foo = new ViewText( 'foo' ); bar = new ViewText( 'bar' ); diff --git a/tests/view/view/view.js b/tests/view/view/view.js index c9ee1248d..36dc07c44 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -30,6 +30,8 @@ describe( 'view', () => { contenteditable: 'true' } ); + document.body.appendChild( domRoot ); + view = new View(); viewDocument = view.document; @@ -61,6 +63,7 @@ describe( 'view', () => { } ); afterEach( () => { + domRoot.remove(); view.destroy(); } ); @@ -238,7 +241,7 @@ describe( 'view', () => { sinon.assert.notCalled( stub ); } ); - it.only( 'scrolls to the first range in selection with an offset', () => { + 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 ); From de945bb4572a588da95a8d336374b4b6398c2a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 10:30:10 +0100 Subject: [PATCH 10/89] Fixed tests with placeholder. --- src/view/placeholder.js | 2 +- src/view/view.js | 2 +- tests/view/placeholder.js | 29 +++++++++++++++++------------ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/view/placeholder.js b/src/view/placeholder.js index 7d2351a62..f6472e68e 100644 --- a/src/view/placeholder.js +++ b/src/view/placeholder.js @@ -45,7 +45,7 @@ export function attachPlaceholder( element, placeholderText, checkFunction ) { // Single listener per document. if ( !documentPlaceholders.has( document ) ) { documentPlaceholders.set( document, new Map() ); - listener.listenTo( document, 'render', () => updateAllPlaceholders( document ), { priority: 'high' } ); + listener.listenTo( document, 'change', () => updateAllPlaceholders( document ) ); } // Store text in element's data attribute. diff --git a/src/view/view.js b/src/view/view.js index a8f9b851b..e9aca7221 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -252,7 +252,7 @@ export default class View { this._ongoingChange = false; // TODO: docs for the event. - this.fire( 'change' ); + this.document.fire( 'change' ); } } diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js index 1adc212a4..194745f6d 100644 --- a/tests/view/placeholder.js +++ b/tests/view/placeholder.js @@ -6,15 +6,16 @@ import { attachPlaceholder, detachPlaceholder } from '../../src/view/placeholder'; import createViewRoot from './_utils/createroot'; import ViewContainerElement from '../../src/view/containerelement'; -import ViewDocument from '../../src/view/document'; +import View from '../../src/view/view'; import ViewRange from '../../src/view/range'; import { setData } from '../../src/dev-utils/view'; describe( 'placeholder', () => { - let viewDocument, viewRoot; + let view, viewDocument, viewRoot; beforeEach( () => { - viewDocument = new ViewDocument(); + view = new View(); + viewDocument = view.document; viewRoot = createViewRoot( viewDocument ); viewDocument.isFocused = true; } ); @@ -100,8 +101,9 @@ describe( 'placeholder', () => { expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; - viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] ); - viewDocument.render(); + view.change( () => { + viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] ); + } ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; } ); @@ -124,13 +126,15 @@ describe( 'placeholder', () => { attachPlaceholder( element, 'foo bar baz' ); setData( viewDocument, '

paragraph

' ); - viewDocument.render(); + view.render(); } ); it( 'should allow to add placeholder to elements from different documents', () => { setData( viewDocument, '
{another div}
' ); const element = viewRoot.getChild( 0 ); - const secondDocument = new ViewDocument(); + + const secondView = new View(); + const secondDocument = secondView.document; secondDocument.isFocused = true; const secondRoot = createViewRoot( secondDocument ); setData( secondDocument, '
{another div}
' ); @@ -146,12 +150,13 @@ describe( 'placeholder', () => { expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true; // Move selection to the elements with placeholders. - viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] ); - secondDocument.selection.setRanges( [ ViewRange.createIn( secondElement ) ] ); + view.change( () => { + viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] ); + } ); - // Render changes. - viewDocument.render(); - secondDocument.render(); + secondView.change( () => { + secondDocument.selection.setRanges( [ ViewRange.createIn( secondElement ) ] ); + } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; From ebe4922424b62160c603faf08f5af7695285bf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 10:42:20 +0100 Subject: [PATCH 11/89] Fixed view Position test. --- tests/view/position.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/view/position.js b/tests/view/position.js index a7574e403..3eb28fd2b 100644 --- a/tests/view/position.js +++ b/tests/view/position.js @@ -515,8 +515,6 @@ describe( 'Position', () => { const position = new Position( p, 0 ); expect( position.editableElement ).to.equal( editable ); - - document.destroy(); } ); } ); From e8c7f7599f3a121625ea1902f3fafa7f235eda4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 10:46:18 +0100 Subject: [PATCH 12/89] Fixed view Selection test. --- tests/view/selection.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/view/selection.js b/tests/view/selection.js index 0f9c47b80..f27088a33 100644 --- a/tests/view/selection.js +++ b/tests/view/selection.js @@ -886,8 +886,6 @@ describe( 'Selection', () => { selection.addRange( Range.createFromParentsAndOffsets( element, 0, element, 0 ) ); expect( selection.editableElement ).to.equal( root ); - - viewDocument.destroy(); } ); } ); From 1025f010bb52209dc68febfd50f678f88d8cf06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 10:51:01 +0100 Subject: [PATCH 13/89] Fixed view TreeWalker test. --- tests/view/treewalker.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/view/treewalker.js b/tests/view/treewalker.js index e527b598f..0516d4866 100644 --- a/tests/view/treewalker.js +++ b/tests/view/treewalker.js @@ -48,10 +48,6 @@ describe( 'TreeWalker', () => { rootEnding = new Position( root, 2 ); } ); - afterEach( () => { - doc.destroy(); - } ); - describe( 'constructor()', () => { it( 'should throw if neither boundaries nor starting position is set', () => { expect( () => { From c8dee0c85d81937ce2902dae0b13c46d0c35144f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 11:37:59 +0100 Subject: [PATCH 14/89] Fixed conversion tests. --- .../model-selection-to-view-converters.js | 5 +++-- src/conversion/model-to-view-converters.js | 4 +++- src/view/view.js | 2 ++ tests/controller/datacontroller.js | 2 +- tests/conversion/advanced-converters.js | 7 ++++--- tests/conversion/buildmodelconverter.js | 6 +++--- tests/conversion/definition-based-converters.js | 4 ++-- .../model-selection-to-view-converters.js | 14 ++++++++------ tests/conversion/model-to-view-converters.js | 4 ++-- .../view-selection-to-model-converters.js | 13 +++++++------ 10 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/conversion/model-selection-to-view-converters.js b/src/conversion/model-selection-to-view-converters.js index 1fe65f9e6..0fed9725e 100644 --- a/src/conversion/model-selection-to-view-converters.js +++ b/src/conversion/model-selection-to-view-converters.js @@ -5,9 +5,11 @@ import ViewElement from '../view/element'; import ViewRange from '../view/range'; -import viewWriter from '../view/writer'; +import ViewWriter from '../view/writer'; import { createViewElementFromHighlightDescriptor } from './model-to-view-converters'; +const viewWriter = new ViewWriter(); + /** * Contains {@link module:engine/model/selection~Selection model selection} to * {@link module:engine/view/selection~Selection view selection} converters for @@ -207,7 +209,6 @@ function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElem viewPosition = viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) ); } // End of hack. - viewPosition = viewWriter.wrapPosition( viewPosition, viewElement ); viewSelection.removeAllRanges(); diff --git a/src/conversion/model-to-view-converters.js b/src/conversion/model-to-view-converters.js index c6ae33c97..1fa55e8cd 100644 --- a/src/conversion/model-to-view-converters.js +++ b/src/conversion/model-to-view-converters.js @@ -9,7 +9,9 @@ import ViewElement from '../view/element'; import ViewAttributeElement from '../view/attributeelement'; import ViewText from '../view/text'; import ViewRange from '../view/range'; -import viewWriter from '../view/writer'; +import WiewWriter from '../view/writer'; + +const viewWriter = new WiewWriter(); /** * Contains model to view converters for diff --git a/src/view/view.js b/src/view/view.js index e9aca7221..d377ddf57 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -35,6 +35,8 @@ export default class View { // TODO: check import path // TODO: check where render() is used and eventually switch to change() where possible // TODO: observers docs fixes + // TODO: check where writer instance is created and it should be returned by change() method only (converters!) + // TODO: manual tests /** * Instance of the {@link module:engine/view/domconverter~DomConverter domConverter} use by diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js index dfa35896d..61f07a15c 100644 --- a/tests/controller/datacontroller.js +++ b/tests/controller/datacontroller.js @@ -44,7 +44,7 @@ describe( 'DataController', () => { } ); describe( 'parse()', () => { - it( 'should set text', () => { + it.only( 'should set text', () => { schema.extend( '$text', { allowIn: '$root' } ); const output = data.parse( '

foobar

' ); diff --git a/tests/conversion/advanced-converters.js b/tests/conversion/advanced-converters.js index c4ac7260f..b5e444eb8 100644 --- a/tests/conversion/advanced-converters.js +++ b/tests/conversion/advanced-converters.js @@ -15,7 +15,7 @@ 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 ViewWriter from '../../src/view/writer'; import ViewPosition from '../../src/view/position'; import ViewRange from '../../src/view/range'; @@ -31,16 +31,17 @@ import { import { convertToModelFragment, convertText } from '../../src/conversion/view-to-model-converters'; describe( 'advanced-converters', () => { - let model, modelDoc, modelRoot, viewRoot, modelDispatcher, viewDispatcher; + let model, modelDoc, modelRoot, viewWriter, viewRoot, modelDispatcher, viewDispatcher; beforeEach( () => { model = new Model(); modelDoc = model.document; modelRoot = modelDoc.createRoot(); + viewWriter = new ViewWriter(); const editing = new EditingController( model ); - viewRoot = editing.view.getRoot(); + viewRoot = editing.view.document.getRoot(); // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. diff --git a/tests/conversion/buildmodelconverter.js b/tests/conversion/buildmodelconverter.js index b2a25299a..479ce0da3 100644 --- a/tests/conversion/buildmodelconverter.js +++ b/tests/conversion/buildmodelconverter.js @@ -66,12 +66,12 @@ describe( 'Model converter builder', () => { // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. - controller.view.getRoot()._name = 'div'; + controller.view.document.getRoot()._name = 'div'; dispatcher = controller.modelToView; - viewRoot = controller.view.getRoot(); - viewSelection = controller.view.selection; + viewRoot = controller.view.document.getRoot(); + viewSelection = controller.view.document.selection; buildModelConverter().for( dispatcher ).fromElement( 'paragraph' ).toElement( 'p' ); } ); diff --git a/tests/conversion/definition-based-converters.js b/tests/conversion/definition-based-converters.js index ea7770850..de1346650 100644 --- a/tests/conversion/definition-based-converters.js +++ b/tests/conversion/definition-based-converters.js @@ -115,9 +115,9 @@ describe( 'definition-based-converters', () => { // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. - controller.view.getRoot()._name = 'div'; + controller.view.document.getRoot()._name = 'div'; - viewRoot = controller.view.getRoot(); + viewRoot = controller.view.document.getRoot(); dispatcher = controller.modelToView; } diff --git a/tests/conversion/model-selection-to-view-converters.js b/tests/conversion/model-selection-to-view-converters.js index 2fe4cd740..2201bce37 100644 --- a/tests/conversion/model-selection-to-view-converters.js +++ b/tests/conversion/model-selection-to-view-converters.js @@ -8,11 +8,11 @@ import ModelElement from '../../src/model/element'; import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; -import ViewDocument from '../../src/view/document'; +import View from '../../src/view/view'; import ViewContainerElement from '../../src/view/containerelement'; import ViewAttributeElement from '../../src/view/attributeelement'; import ViewUIElement from '../../src/view/uielement'; -import { mergeAttributes } from '../../src/view/writer'; +import ViewWriter from '../../src/view/writer'; import Mapper from '../../src/conversion/mapper'; import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher'; @@ -39,7 +39,7 @@ import { stringify as stringifyView } from '../../src/dev-utils/view'; import { setData as setModelData } from '../../src/dev-utils/model'; describe( 'model-selection-to-view-converters', () => { - let dispatcher, mapper, model, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor; + let dispatcher, mapper, model, view, modelDoc, modelRoot, modelSelection, viewDoc, viewRoot, viewSelection, highlightDescriptor; beforeEach( () => { model = new Model(); @@ -49,7 +49,8 @@ describe( 'model-selection-to-view-converters', () => { model.schema.extend( '$text', { allowIn: '$root' } ); - viewDoc = new ViewDocument(); + view = new View(); + viewDoc = view.document; viewRoot = createViewRoot( viewDoc ); viewSelection = viewDoc.selection; @@ -74,7 +75,7 @@ describe( 'model-selection-to-view-converters', () => { } ); afterEach( () => { - viewDoc.destroy(); + view.destroy(); } ); describe( 'default converters', () => { @@ -501,7 +502,8 @@ describe( 'model-selection-to-view-converters', () => { ); // Remove manually. - mergeAttributes( viewSelection.getFirstPosition() ); + const viewWriter = new ViewWriter(); + viewWriter.mergeAttributes( viewSelection.getFirstPosition() ); const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 ); modelDoc.selection.setRanges( [ modelRange ] ); diff --git a/tests/conversion/model-to-view-converters.js b/tests/conversion/model-to-view-converters.js index a0a82eea3..811745856 100644 --- a/tests/conversion/model-to-view-converters.js +++ b/tests/conversion/model-to-view-converters.js @@ -39,10 +39,10 @@ describe( 'model-to-view-converters', () => { controller = new EditingController( model ); - viewRoot = controller.view.getRoot(); + viewRoot = controller.view.document.getRoot(); // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. - controller.view.getRoot()._name = 'div'; + controller.view.document.getRoot()._name = 'div'; dispatcher = controller.modelToView; diff --git a/tests/conversion/view-selection-to-model-converters.js b/tests/conversion/view-selection-to-model-converters.js index 947e03c2e..d6dc43a9c 100644 --- a/tests/conversion/view-selection-to-model-converters.js +++ b/tests/conversion/view-selection-to-model-converters.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import ViewDocument from '../../src/view/document'; +import View from '../../src/view/view'; import ViewSelection from '../../src/view/selection'; import ViewRange from '../../src/view/range'; import createViewRoot from '../view/_utils/createroot'; @@ -17,7 +17,7 @@ import { setData as modelSetData, getData as modelGetData } from '../../src/dev- import { setData as viewSetData } from '../../src/dev-utils/view'; describe( 'convertSelectionChange', () => { - let model, view, mapper, convertSelection, modelRoot, viewRoot; + let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; beforeEach( () => { model = new Model(); @@ -26,10 +26,11 @@ describe( 'convertSelectionChange', () => { modelSetData( model, 'foobar' ); - view = new ViewDocument(); - viewRoot = createViewRoot( view, 'div', 'main' ); + view = new View(); + viewDocument = view.document; + viewRoot = createViewRoot( viewDocument, 'div', 'main' ); - viewSetData( view, '

foo

bar

' ); + viewSetData( viewDocument, '

foo

bar

' ); mapper = new Mapper(); mapper.bindElements( modelRoot, viewRoot ); @@ -56,7 +57,7 @@ describe( 'convertSelectionChange', () => { it( 'should support unicode', () => { modelSetData( model, 'நிலைக்கு' ); - viewSetData( view, '

நிலைக்கு

' ); + viewSetData( viewDocument, '

நிலைக்கு

' ); // Re-bind elements that were just re-set. mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); From eb3290d57afc4baf5fdeb4428df50086232b6ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 12:19:07 +0100 Subject: [PATCH 15/89] Fixed dev utils tests. --- src/dev-utils/enableenginedebug.js | 5 +++-- tests/dev-utils/enableenginedebug.js | 3 ++- tests/dev-utils/view.js | 22 +++++++++++++--------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/dev-utils/enableenginedebug.js b/src/dev-utils/enableenginedebug.js index be878ec0b..adb407173 100644 --- a/src/dev-utils/enableenginedebug.js +++ b/src/dev-utils/enableenginedebug.js @@ -628,7 +628,7 @@ function enableDocumentTools() { } ); sandbox.mock( Editor.prototype, 'logView', function( version ) { - this.editing.view.log( version ); + this.editing.view.document.log( version ); } ); sandbox.mock( Editor.prototype, 'logDocuments', function( version = null ) { @@ -658,7 +658,8 @@ class DebugPlugin extends Plugin { const model = this.editor.model; const modelDocument = model.document; - const viewDocument = this.editor.editing.view; + const view = this.editor.editing.view; + const viewDocument = view.document; modelDocument[ treeDump ] = []; viewDocument[ treeDump ] = []; diff --git a/tests/dev-utils/enableenginedebug.js b/tests/dev-utils/enableenginedebug.js index 4f762dc12..29033436a 100644 --- a/tests/dev-utils/enableenginedebug.js +++ b/tests/dev-utils/enableenginedebug.js @@ -794,7 +794,8 @@ describe( 'debug tools', () => { const model = editor.model; const modelDoc = model.document; const modelRoot = modelDoc.getRoot(); - const viewDoc = editor.editing.view; + const view = editor.editing.view; + const viewDoc = view.document; model.change( () => { const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 ); diff --git a/tests/dev-utils/view.js b/tests/dev-utils/view.js index 816eacf89..ce0ce493c 100644 --- a/tests/dev-utils/view.js +++ b/tests/dev-utils/view.js @@ -16,7 +16,7 @@ import UIElement from '../../src/view/uielement'; import Text from '../../src/view/text'; import Selection from '../../src/view/selection'; import Range from '../../src/view/range'; -import Document from '../../src/view/document'; +import View from '../../src/view/view'; import XmlDataProcessor from '../../src/dataprocessor/xmldataprocessor'; import createViewRoot from '../view/_utils/createroot'; @@ -36,7 +36,8 @@ describe( 'view test utils', () => { it( 'should use stringify method', () => { const element = document.createElement( 'div' ); const stringifySpy = sandbox.spy( getData, '_stringify' ); - const viewDocument = new Document(); + const view = new View(); + const viewDocument = view.document; const options = { showType: false, showPriority: false, withoutSelection: true }; const root = createAttachedRoot( viewDocument, element ); root.appendChildren( new Element( 'p' ) ); @@ -50,13 +51,14 @@ describe( 'view test utils', () => { expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false ); expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true ); - viewDocument.destroy(); + view.destroy(); } ); it( 'should use stringify method with selection', () => { const element = document.createElement( 'div' ); const stringifySpy = sandbox.spy( getData, '_stringify' ); - const viewDocument = new Document(); + const view = new View(); + const viewDocument = view.document; const options = { showType: false, showPriority: false }; const root = createAttachedRoot( viewDocument, element ); root.appendChildren( new Element( 'p' ) ); @@ -72,7 +74,7 @@ describe( 'view test utils', () => { expect( stringifyOptions ).to.have.property( 'showPriority' ).that.equals( false ); expect( stringifyOptions ).to.have.property( 'ignoreRoot' ).that.equals( true ); - viewDocument.destroy(); + view.destroy(); } ); it( 'should throw an error when passing invalid document', () => { @@ -84,7 +86,8 @@ describe( 'view test utils', () => { describe( 'setData', () => { it( 'should use parse method', () => { - const viewDocument = new Document(); + const view = new View(); + const viewDocument = view.document; const data = 'foobarbaz'; const parseSpy = sandbox.spy( setData, '_parse' ); @@ -98,11 +101,12 @@ describe( 'view test utils', () => { expect( args[ 1 ] ).to.be.an( 'object' ); expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() ); - viewDocument.destroy(); + view.destroy(); } ); it( 'should use parse method with selection', () => { - const viewDocument = new Document(); + const view = new View(); + const viewDocument = view.document; const data = '[baz]'; const parseSpy = sandbox.spy( setData, '_parse' ); @@ -115,7 +119,7 @@ describe( 'view test utils', () => { expect( args[ 1 ] ).to.be.an( 'object' ); expect( args[ 1 ].rootElement ).to.equal( viewDocument.getRoot() ); - viewDocument.destroy(); + view.destroy(); } ); it( 'should throw an error when passing invalid document', () => { From e7f0ba9f16d90e21b3f84e8d2e21c30d3b856a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 13:20:45 +0100 Subject: [PATCH 16/89] Controller fixes. --- tests/controller/datacontroller.js | 2 +- tests/controller/editingcontroller.js | 54 +++++++++++++-------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/controller/datacontroller.js b/tests/controller/datacontroller.js index 61f07a15c..dfa35896d 100644 --- a/tests/controller/datacontroller.js +++ b/tests/controller/datacontroller.js @@ -44,7 +44,7 @@ describe( 'DataController', () => { } ); describe( 'parse()', () => { - it.only( 'should set text', () => { + it( 'should set text', () => { schema.extend( '$text', { allowIn: '$root' } ); const output = data.parse( '

foobar

' ); diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js index 7783aad35..74f6b97b7 100644 --- a/tests/controller/editingcontroller.js +++ b/tests/controller/editingcontroller.js @@ -9,7 +9,7 @@ import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; import EditingController from '../../src/controller/editingcontroller'; -import ViewDocument from '../../src/view/document'; +import View from '../../src/view/view'; import Mapper from '../../src/conversion/mapper'; import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher'; @@ -38,7 +38,7 @@ describe( 'EditingController', () => { it( 'should create controller with properties', () => { expect( editing ).to.have.property( 'model' ).that.equals( model ); - expect( editing ).to.have.property( 'view' ).that.is.instanceof( ViewDocument ); + expect( editing ).to.have.property( 'view' ).that.is.instanceof( View ); expect( editing ).to.have.property( 'mapper' ).that.is.instanceof( Mapper ); expect( editing ).to.have.property( 'modelToView' ).that.is.instanceof( ModelConversionDispatcher ); @@ -56,15 +56,15 @@ describe( 'EditingController', () => { it( 'should bind view roots to model roots', () => { expect( model.document.roots ).to.length( 1 ); // $graveyard - expect( editing.view.roots ).to.length( 0 ); + expect( editing.view.document.roots ).to.length( 0 ); const modelRoot = model.document.createRoot(); expect( model.document.roots ).to.length( 2 ); - expect( editing.view.roots ).to.length( 1 ); - expect( editing.view.getRoot().document ).to.equal( editing.view ); + expect( editing.view.document.roots ).to.length( 1 ); + expect( editing.view.document.getRoot().document ).to.equal( editing.view.document ); - expect( editing.view.getRoot().name ).to.equal( modelRoot.name ).to.equal( '$root' ); + expect( editing.view.document.getRoot().name ).to.equal( modelRoot.name ).to.equal( '$root' ); } ); } ); @@ -84,7 +84,7 @@ describe( 'EditingController', () => { document.body.appendChild( domRoot ); - viewRoot = editing.view.getRoot(); + viewRoot = editing.view.document.getRoot(); editing.view.attachDomRoot( domRoot ); model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); @@ -122,11 +122,11 @@ describe( 'EditingController', () => { } ); it( 'should convert insertion', () => { - expect( getViewData( editing.view ) ).to.equal( '

f{}oo

bar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

f{}oo

bar

' ); } ); it( 'should convert split', () => { - expect( getViewData( editing.view ) ).to.equal( '

f{}oo

bar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

f{}oo

bar

' ); model.change( writer => { writer.split( model.document.selection.getFirstPosition() ); @@ -136,17 +136,17 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view ) ).to.equal( '

f

{}oo

bar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

f

{}oo

bar

' ); } ); it( 'should convert rename', () => { - expect( getViewData( editing.view ) ).to.equal( '

f{}oo

bar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

f{}oo

bar

' ); model.change( writer => { writer.rename( modelRoot.getChild( 0 ), 'div' ); } ); - expect( getViewData( editing.view ) ).to.equal( '
f{}oo

bar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '
f{}oo

bar

' ); } ); it( 'should convert delete', () => { @@ -160,11 +160,11 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view ) ).to.equal( '

f{}o

bar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

f{}o

bar

' ); } ); it( 'should convert selection from view to model', done => { - listener.listenTo( editing.view, 'selectionChange', () => { + listener.listenTo( editing.view.document, 'selectionChange', () => { setTimeout( () => { expect( getModelData( model ) ).to.equal( 'foo' + @@ -173,10 +173,10 @@ describe( 'EditingController', () => { ); done(); - } ); + }, 1 ); } ); - editing.view.isFocused = true; + editing.view.document.isFocused = true; editing.view.render(); const domSelection = document.getSelection(); @@ -195,7 +195,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view ) ).to.equal( '

foo

b{}ar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

foo

b{}ar

' ); } ); it( 'should convert not collapsed selection', () => { @@ -205,7 +205,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view ) ).to.equal( '

foo

b{a}r

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

foo

b{a}r

' ); } ); it( 'should clear previous selection', () => { @@ -215,7 +215,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view ) ).to.equal( '

foo

b{}ar

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

foo

b{}ar

' ); model.change( () => { model.document.selection.setRanges( [ @@ -223,7 +223,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view ) ).to.equal( '

foo

ba{}r

' ); + expect( getViewData( editing.view.document ) ).to.equal( '

foo

ba{}r

' ); } ); it( 'should convert adding marker', () => { @@ -233,7 +233,7 @@ describe( 'EditingController', () => { model.markers.set( 'marker', range ); } ); - expect( getViewData( editing.view, { withoutSelection: true } ) ) + expect( getViewData( editing.view.document, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); @@ -248,7 +248,7 @@ describe( 'EditingController', () => { model.markers.remove( 'marker' ); } ); - expect( getViewData( editing.view, { withoutSelection: true } ) ) + expect( getViewData( editing.view.document, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); @@ -265,7 +265,7 @@ describe( 'EditingController', () => { model.markers.set( 'marker', range2 ); } ); - expect( getViewData( editing.view, { withoutSelection: true } ) ) + expect( getViewData( editing.view.document, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); @@ -280,7 +280,7 @@ describe( 'EditingController', () => { writer.insertText( 'xyz', new ModelPosition( modelRoot, [ 1, 0 ] ) ); } ); - expect( getViewData( editing.view, { withoutSelection: true } ) ) + expect( getViewData( editing.view.document, { withoutSelection: true } ) ) .to.equal( '

foo

xyz

bar

' ); } ); @@ -298,7 +298,7 @@ describe( 'EditingController', () => { ); } ); - expect( getViewData( editing.view, { withoutSelection: true } ) ) + expect( getViewData( editing.view.document, { withoutSelection: true } ) ) .to.equal( '

foor

ba

' ); } ); @@ -316,7 +316,7 @@ describe( 'EditingController', () => { ); } ); - expect( getViewData( editing.view, { withoutSelection: true } ) ) + expect( getViewData( editing.view.document, { withoutSelection: true } ) ) .to.equal( '

f

baroo

' ); } ); @@ -334,7 +334,7 @@ describe( 'EditingController', () => { ); } ); - expect( getViewData( editing.view, { withoutSelection: true } ) ) + expect( getViewData( editing.view.document, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); } ); From 19af78b9b90de561f138b43301dac5e4e72f7f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 16 Jan 2018 13:26:26 +0100 Subject: [PATCH 17/89] Adjusting ticket test to changes in view. --- tests/tickets/699.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tickets/699.js b/tests/tickets/699.js index 87ca47eba..12623977a 100644 --- a/tests/tickets/699.js +++ b/tests/tickets/699.js @@ -34,7 +34,7 @@ describe( 'Bug ckeditor5-engine#699', () => { editor.setData( '

foo

' ); expect( getModelData( editor.model ) ).to.equal( '[]foo' ); - expect( getViewData( editor.editing.view ) ).to.equal( '[]

foo

' ); + expect( getViewData( editor.editing.view.document ) ).to.equal( '[]

foo

' ); return editor.destroy(); } ); From 8eb46b168ef5bbb71a894b92394b7f35d7020f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Thu, 18 Jan 2018 16:32:48 +0100 Subject: [PATCH 18/89] Passing view writer with conversion API. --- src/controller/editingcontroller.js | 5 +- .../model-selection-to-view-converters.js | 29 +- src/conversion/model-to-view-converters.js | 28 +- src/conversion/modelconversiondispatcher.js | 251 ++++++++++-------- src/dev-utils/model.js | 4 +- .../model-selection-to-view-converters.js | 2 +- tests/conversion/modelconversiondispatcher.js | 8 +- 7 files changed, 184 insertions(+), 143 deletions(-) diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index bf337051c..ae9a5018f 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -78,7 +78,7 @@ export default class EditingController { * @readonly * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} #modelToView */ - this.modelToView = new ModelConversionDispatcher( this.model, { + this.modelToView = new ModelConversionDispatcher( this.model, this.view, { mapper: this.mapper, viewSelection: this.view.document.selection } ); @@ -92,9 +92,6 @@ export default class EditingController { // After the view is ready, convert selection from model to view. this.modelToView.convertSelection( doc.selection ); - - // When everything is converted to the view, render it to DOM. - this.view.render(); }, { priority: 'low' } ); // Convert selection from view to model. diff --git a/src/conversion/model-selection-to-view-converters.js b/src/conversion/model-selection-to-view-converters.js index 0fed9725e..76cf79015 100644 --- a/src/conversion/model-selection-to-view-converters.js +++ b/src/conversion/model-selection-to-view-converters.js @@ -5,11 +5,8 @@ import ViewElement from '../view/element'; import ViewRange from '../view/range'; -import ViewWriter from '../view/writer'; import { createViewElementFromHighlightDescriptor } from './model-to-view-converters'; -const viewWriter = new ViewWriter(); - /** * Contains {@link module:engine/model/selection~Selection model selection} to * {@link module:engine/view/selection~Selection view selection} converters for @@ -86,7 +83,7 @@ export function convertCollapsedSelection() { const modelPosition = selection.getFirstPosition(); const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); - const brokenPosition = viewWriter.breakAttributes( viewPosition ); + const brokenPosition = conversionApi.writer.breakAttributes( viewPosition ); conversionApi.viewSelection.removeAllRanges(); conversionApi.viewSelection.addRange( new ViewRange( brokenPosition, brokenPosition ) ); @@ -153,7 +150,14 @@ export function convertSelectionAttribute( elementCreator ) { const consumableName = 'selectionAttribute:' + data.key; - wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName ); + wrapCollapsedSelectionPosition( + data.selection, + conversionApi.viewSelection, + viewElement, + consumable, + consumableName, + conversionApi.writer + ); }; } @@ -185,12 +189,19 @@ export function convertSelectionMarker( highlightDescriptor ) { const viewElement = createViewElementFromHighlightDescriptor( descriptor ); const consumableName = 'selectionMarker:' + data.markerName; - wrapCollapsedSelectionPosition( data.selection, conversionApi.viewSelection, viewElement, consumable, consumableName ); + wrapCollapsedSelectionPosition( + data.selection, + conversionApi.viewSelection, + viewElement, + consumable, + consumableName, + conversionApi.writer + ); }; } // Helper function for `convertSelectionAttribute` and `convertSelectionMarker`, which perform similar task. -function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName ) { +function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElement, consumable, consumableName, writer ) { if ( !modelSelection.isCollapsed ) { return; } @@ -209,7 +220,7 @@ function wrapCollapsedSelectionPosition( modelSelection, viewSelection, viewElem viewPosition = viewPosition.getLastMatchingPosition( value => value.item.is( 'uiElement' ) ); } // End of hack. - viewPosition = viewWriter.wrapPosition( viewPosition, viewElement ); + viewPosition = writer.wrapPosition( viewPosition, viewElement ); viewSelection.removeAllRanges(); viewSelection.addRange( new ViewRange( viewPosition, viewPosition ) ); @@ -260,7 +271,7 @@ export function clearAttributes() { if ( range.isCollapsed ) { // Position might be in the node removed by the view writer. if ( range.end.parent.document ) { - viewWriter.mergeAttributes( range.start ); + conversionApi.writer.mergeAttributes( range.start ); } } } diff --git a/src/conversion/model-to-view-converters.js b/src/conversion/model-to-view-converters.js index 1fa55e8cd..ce5904559 100644 --- a/src/conversion/model-to-view-converters.js +++ b/src/conversion/model-to-view-converters.js @@ -9,9 +9,6 @@ import ViewElement from '../view/element'; import ViewAttributeElement from '../view/attributeelement'; import ViewText from '../view/text'; import ViewRange from '../view/range'; -import WiewWriter from '../view/writer'; - -const viewWriter = new WiewWriter(); /** * Contains model to view converters for @@ -65,7 +62,7 @@ export function insertElement( elementCreator ) { const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); conversionApi.mapper.bindElements( data.item, viewElement ); - viewWriter.insert( viewPosition, viewElement ); + conversionApi.writer.insert( viewPosition, viewElement ); }; } @@ -88,7 +85,7 @@ export function insertText() { const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); const viewText = new ViewText( data.item.data ); - viewWriter.insert( viewPosition, viewText ); + conversionApi.writer.insert( viewPosition, viewText ); }; } @@ -110,7 +107,7 @@ export function remove() { const viewRange = new ViewRange( viewStart, viewEnd ); // Trim the range to remove in case some UI elements are on the view range boundaries. - const removed = viewWriter.remove( viewRange.getTrimmed() ); + const removed = conversionApi.writer.remove( viewRange.getTrimmed() ); // After the range is removed, unbind all view elements from the model. // Range inside view document fragment is used to unbind deeply. @@ -172,13 +169,14 @@ export function insertUIElement( elementCreator ) { } const mapper = conversionApi.mapper; + const writer = conversionApi.writer; // Add "opening" element. - viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement ); + writer.insert( mapper.toViewPosition( markerRange.start ), viewStartElement ); // Add "closing" element only if range is not collapsed. if ( !markerRange.isCollapsed ) { - viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement ); + writer.insert( mapper.toViewPosition( markerRange.end ), viewEndElement ); } evt.stop(); @@ -215,15 +213,16 @@ export function removeUIElement( elementCreator ) { } const markerRange = data.markerRange; + const writer = conversionApi.writer; // When removing the ui elements, we map the model range to view twice, because that view range // may change after the first clearing. if ( !markerRange.isCollapsed ) { - viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement ); + writer.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement ); } // Remove "opening" element. - viewWriter.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement ); + writer.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement ); evt.stop(); }; @@ -329,15 +328,16 @@ export function wrap( elementCreator ) { } let viewRange = conversionApi.mapper.toViewRange( data.range ); + const writer = conversionApi.writer; // First, unwrap the range from current wrapper. if ( data.attributeOldValue !== null ) { - viewRange = viewWriter.unwrap( viewRange, oldViewElement ); + viewRange = writer.unwrap( viewRange, oldViewElement ); } // Then wrap with the new wrapper. if ( data.attributeNewValue !== null ) { - viewWriter.wrap( viewRange, newViewElement ); + writer.wrap( viewRange, newViewElement ); } }; } @@ -379,7 +379,7 @@ export function highlightText( highlightDescriptor ) { const viewElement = createViewElementFromHighlightDescriptor( descriptor ); const viewRange = conversionApi.mapper.toViewRange( data.range ); - viewWriter.wrap( viewRange, viewElement ); + conversionApi.writer.wrap( viewRange, viewElement ); }; } @@ -496,7 +496,7 @@ export function removeHighlight( highlightDescriptor ) { for ( const item of Array.from( items ).reverse() ) { if ( item.is( 'textProxy' ) ) { - viewWriter.unwrap( ViewRange.createOn( item ), viewHighlightElement ); + conversionApi.writer.unwrap( ViewRange.createOn( item ), viewHighlightElement ); } } }; diff --git a/src/conversion/modelconversiondispatcher.js b/src/conversion/modelconversiondispatcher.js index 90e337e64..57709aa97 100644 --- a/src/conversion/modelconversiondispatcher.js +++ b/src/conversion/modelconversiondispatcher.js @@ -105,7 +105,7 @@ export default class ModelConversionDispatcher { * @param {module:engine/model/model~Model} model Data model. * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls. */ - constructor( model, conversionApi = {} ) { + constructor( model, view, conversionApi = {} ) { /** * Data model instance bound with this dispatcher. * @@ -114,6 +114,8 @@ export default class ModelConversionDispatcher { */ this._model = model; + this._view = view; + /** * Interface passed by dispatcher to the events callbacks. * @@ -128,27 +130,31 @@ export default class ModelConversionDispatcher { * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes. */ convertChanges( differ ) { - // First, before changing view structure, remove all markers that has changed. - for ( const change of differ.getMarkersToRemove() ) { - this.convertMarkerRemove( change.name, change.range ); - } + this._view.change( writer => { + this.conversionApi.writer = writer; - // Convert changes that happened on model tree. - for ( const entry of differ.getChanges() ) { - if ( entry.type == 'insert' ) { - this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ) ); - } else if ( entry.type == 'remove' ) { - this.convertRemove( entry.position, entry.length, entry.name ); - } else { - // entry.type == 'attribute'. - this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue ); + // First, before changing view structure, remove all markers that has changed. + for ( const change of differ.getMarkersToRemove() ) { + this.convertMarkerRemove( change.name, change.range ); } - } - // After the view is updated, convert markers which has changed. - for ( const change of differ.getMarkersToAdd() ) { - this.convertMarkerAdd( change.name, change.range ); - } + // Convert changes that happened on model tree. + for ( const entry of differ.getChanges() ) { + if ( entry.type == 'insert' ) { + this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ) ); + } else if ( entry.type == 'remove' ) { + this.convertRemove( entry.position, entry.length, entry.name ); + } else { + // entry.type == 'attribute'. + this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue ); + } + } + + // After the view is updated, convert markers which has changed. + for ( const change of differ.getMarkersToAdd() ) { + this.convertMarkerAdd( change.name, change.range ); + } + } ); } /** @@ -162,31 +168,35 @@ export default class ModelConversionDispatcher { * @param {module:engine/model/range~Range} range Inserted range. */ convertInsert( range ) { - // Create a list of things that can be consumed, consisting of nodes and their attributes. - const consumable = this._createInsertConsumable( range ); - - // Fire a separate insert event for each node and text fragment contained in the range. - for ( const value of range ) { - const item = value.item; - const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); - const data = { - item, - range: itemRange - }; - - this._testAndFire( 'insert', data, consumable ); - - // Fire a separate addAttribute event for each attribute that was set on inserted items. - // This is important because most attributes converters will listen only to add/change/removeAttribute events. - // If we would not add this part, attributes on inserted nodes would not be converted. - for ( const key of item.getAttributeKeys() ) { - data.attributeKey = key; - data.attributeOldValue = null; - data.attributeNewValue = item.getAttribute( key ); - - this._testAndFire( `attribute:${ key }`, data, consumable ); + this._view.change( writer => { + this.conversionApi.writer = writer; + + // Create a list of things that can be consumed, consisting of nodes and their attributes. + const consumable = this._createInsertConsumable( range ); + + // Fire a separate insert event for each node and text fragment contained in the range. + for ( const value of range ) { + const item = value.item; + const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); + const data = { + item, + range: itemRange + }; + + this._testAndFire( 'insert', data, consumable ); + + // Fire a separate addAttribute event for each attribute that was set on inserted items. + // This is important because most attributes converters will listen only to add/change/removeAttribute events. + // If we would not add this part, attributes on inserted nodes would not be converted. + for ( const key of item.getAttributeKeys() ) { + data.attributeKey = key; + data.attributeOldValue = null; + data.attributeNewValue = item.getAttribute( key ); + + this._testAndFire( `attribute:${ key }`, data, consumable ); + } } - } + } ); } /** @@ -197,7 +207,11 @@ export default class ModelConversionDispatcher { * @param {String} name Name of removed node. */ convertRemove( position, length, name ) { - this.fire( 'remove:' + name, { position, length }, this.conversionApi ); + this._view.change( writer => { + this.conversionApi.writer = writer; + + this.fire( 'remove:' + name, { position, length }, this.conversionApi ); + } ); } /** @@ -212,23 +226,27 @@ export default class ModelConversionDispatcher { * @param {*} newValue New attribute value or `null` if the attribute has been removed. */ convertAttribute( range, key, oldValue, newValue ) { - // Create a list with attributes to consume. - const consumable = this._createConsumableForRange( range, `attribute:${ key }` ); + this._view.change( writer => { + this.conversionApi.writer = writer; + + // Create a list with attributes to consume. + const consumable = this._createConsumableForRange( range, `attribute:${ key }` ); + + // Create a separate attribute event for each node in the range. + for ( const value of range ) { + const item = value.item; + const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); + const data = { + item, + range: itemRange, + attributeKey: key, + attributeOldValue: oldValue, + attributeNewValue: newValue + }; - // Create a separate attribute event for each node in the range. - for ( const value of range ) { - const item = value.item; - const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); - const data = { - item, - range: itemRange, - attributeKey: key, - attributeOldValue: oldValue, - attributeNewValue: newValue - }; - - this._testAndFire( `attribute:${ key }`, data, consumable ); - } + this._testAndFire( `attribute:${ key }`, data, consumable ); + } + } ); } /** @@ -242,41 +260,44 @@ export default class ModelConversionDispatcher { * @param {module:engine/model/selection~Selection} selection Selection to convert. */ convertSelection( selection ) { - const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) ); - const consumable = this._createSelectionConsumable( selection, markers ); + this._view.change( writer => { + this.conversionApi.writer = writer; + const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) ); + const consumable = this._createSelectionConsumable( selection, markers ); - this.fire( 'selection', { selection }, consumable, this.conversionApi ); + this.fire( 'selection', { selection }, consumable, this.conversionApi ); - for ( const marker of markers ) { - const markerRange = marker.getRange(); + for ( const marker of markers ) { + const markerRange = marker.getRange(); - if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) { - continue; - } + if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) { + continue; + } - const data = { - selection, - markerName: marker.name, - markerRange - }; + const data = { + selection, + markerName: marker.name, + markerRange + }; - if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) { - this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi ); + if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) { + this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi ); + } } - } - for ( const key of selection.getAttributeKeys() ) { - const data = { - selection, - key, - value: selection.getAttribute( key ) - }; - - // Do not fire event if the attribute has been consumed. - if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) { - this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi ); + for ( const key of selection.getAttributeKeys() ) { + const data = { + selection, + key, + value: selection.getAttribute( key ) + }; + + // Do not fire event if the attribute has been consumed. + if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) { + this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi ); + } } - } + } ); } /** @@ -293,36 +314,40 @@ export default class ModelConversionDispatcher { return; } - // In markers' case, event name == consumable name. - const eventName = 'addMarker:' + markerName; + this._view.change( writer => { + this.conversionApi.writer = writer; - // When range is collapsed - fire single event with collapsed range in consumable. - if ( markerRange.isCollapsed ) { - const consumable = new Consumable(); - consumable.add( markerRange, eventName ); + // In markers' case, event name == consumable name. + const eventName = 'addMarker:' + markerName; - this.fire( eventName, { - markerName, - markerRange - }, consumable, this.conversionApi ); + // When range is collapsed - fire single event with collapsed range in consumable. + if ( markerRange.isCollapsed ) { + const consumable = new Consumable(); + consumable.add( markerRange, eventName ); - return; - } + this.fire( eventName, { + markerName, + markerRange + }, consumable, this.conversionApi ); - // Create consumable for each item in range. - const consumable = this._createConsumableForRange( markerRange, eventName ); - - // Create separate event for each node in the range. - for ( const item of markerRange.getItems() ) { - // Do not fire event for already consumed items. - if ( !consumable.test( item, eventName ) ) { - continue; + return; } - const data = { item, range: Range.createOn( item ), markerName, markerRange }; + // Create consumable for each item in range. + const consumable = this._createConsumableForRange( markerRange, eventName ); - this.fire( eventName, data, consumable, this.conversionApi ); - } + // Create separate event for each node in the range. + for ( const item of markerRange.getItems() ) { + // Do not fire event for already consumed items. + if ( !consumable.test( item, eventName ) ) { + continue; + } + + const data = { item, range: Range.createOn( item ), markerName, markerRange }; + + this.fire( eventName, data, consumable, this.conversionApi ); + } + } ); } /** @@ -338,7 +363,11 @@ export default class ModelConversionDispatcher { return; } - this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi ); + this._view.change( writer => { + this.conversionApi.writer = writer; + + this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi ); + } ); } /** diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index caa6f197a..5e8181b30 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -20,6 +20,7 @@ import ModelConversionDispatcher from '../conversion/modelconversiondispatcher'; import ModelSelection from '../model/selection'; import ModelDocumentFragment from '../model/documentfragment'; +import View from '../view/view'; import ViewConversionDispatcher from '../conversion/viewconversiondispatcher'; import ViewSelection from '../view/selection'; import ViewDocumentFragment from '../view/documentfragment'; @@ -159,6 +160,7 @@ setData._parse = parse; */ export function stringify( node, selectionOrPositionOrRange = null ) { const model = new Model(); + const view = new View(); const mapper = new Mapper(); let selection, range; @@ -192,7 +194,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) { // Setup model to view converter. const viewDocumentFragment = new ViewDocumentFragment(); const viewSelection = new ViewSelection(); - const modelToView = new ModelConversionDispatcher( model, { mapper, viewSelection } ); + const modelToView = new ModelConversionDispatcher( model, view, { mapper, viewSelection } ); // Bind root elements. mapper.bindElements( node.root, viewDocumentFragment ); diff --git a/tests/conversion/model-selection-to-view-converters.js b/tests/conversion/model-selection-to-view-converters.js index 2201bce37..454f1ba60 100644 --- a/tests/conversion/model-selection-to-view-converters.js +++ b/tests/conversion/model-selection-to-view-converters.js @@ -59,7 +59,7 @@ describe( 'model-selection-to-view-converters', () => { highlightDescriptor = { class: 'marker', priority: 1 }; - dispatcher = new ModelConversionDispatcher( model, { mapper, viewSelection } ); + dispatcher = new ModelConversionDispatcher( model, view, { mapper, viewSelection } ); dispatcher.on( 'insert:$text', insertText() ); dispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) ); diff --git a/tests/conversion/modelconversiondispatcher.js b/tests/conversion/modelconversiondispatcher.js index 9ca3e3ca8..fb06f527e 100644 --- a/tests/conversion/modelconversiondispatcher.js +++ b/tests/conversion/modelconversiondispatcher.js @@ -10,15 +10,17 @@ import ModelElement from '../../src/model/element'; import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; +import View from '../../src/view/view'; import ViewContainerElement from '../../src/view/containerelement'; describe( 'ModelConversionDispatcher', () => { - let dispatcher, doc, root, differStub, model; + let dispatcher, doc, root, differStub, model, view; beforeEach( () => { model = new Model(); + view = new View(); doc = model.document; - dispatcher = new ModelConversionDispatcher( model ); + dispatcher = new ModelConversionDispatcher( model, view ); root = doc.createRoot(); differStub = { @@ -31,7 +33,7 @@ describe( 'ModelConversionDispatcher', () => { describe( 'constructor()', () => { it( 'should create ModelConversionDispatcher with given api', () => { const apiObj = {}; - const dispatcher = new ModelConversionDispatcher( model, { apiObj } ); + const dispatcher = new ModelConversionDispatcher( model, view, { apiObj } ); expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj ); } ); From 6de4eff33e90fbec86add7828358bf5a4480d6ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Fri, 19 Jan 2018 13:19:29 +0100 Subject: [PATCH 19/89] Removed view and change() block from ModelConversionDispatcher. Now writer is passed as parameter to conversion methods. --- src/controller/datacontroller.js | 3 +- src/controller/editingcontroller.js | 12 +- src/conversion/modelconversiondispatcher.js | 280 +++++++++--------- src/dev-utils/model.js | 10 +- tests/conversion/advanced-converters.js | 2 +- .../model-selection-to-view-converters.js | 116 ++++---- tests/conversion/modelconversiondispatcher.js | 28 +- 7 files changed, 230 insertions(+), 221 deletions(-) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index ea41659a1..408484297 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 ViewWriter from '../view/writer'; import ModelRange from '../model/range'; @@ -158,7 +159,7 @@ export default class DataController { const viewDocumentFragment = new ViewDocumentFragment(); this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment ); - this.modelToView.convertInsert( modelRange ); + this.modelToView.convertInsert( modelRange, new ViewWriter() ); this.mapper.clearBindings(); diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index ae9a5018f..e4ebd78d5 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -78,7 +78,7 @@ export default class EditingController { * @readonly * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} #modelToView */ - this.modelToView = new ModelConversionDispatcher( this.model, this.view, { + this.modelToView = new ModelConversionDispatcher( this.model, { mapper: this.mapper, viewSelection: this.view.document.selection } ); @@ -87,11 +87,13 @@ export default class EditingController { // When all changes are done, get the model diff containing all the changes and convert them to view and then render to DOM. this.listenTo( doc, 'change', () => { - // Convert changes stored in `modelDiffer`. - this.modelToView.convertChanges( doc.differ ); + this.view.change( writer => { + // Convert changes stored in `modelDiffer`. + this.modelToView.convertChanges( doc.differ, writer ); - // After the view is ready, convert selection from model to view. - this.modelToView.convertSelection( doc.selection ); + // After the view is ready, convert selection from model to view. + this.modelToView.convertSelection( doc.selection, writer ); + } ); }, { priority: 'low' } ); // Convert selection from view to model. diff --git a/src/conversion/modelconversiondispatcher.js b/src/conversion/modelconversiondispatcher.js index 57709aa97..09b2edf93 100644 --- a/src/conversion/modelconversiondispatcher.js +++ b/src/conversion/modelconversiondispatcher.js @@ -105,7 +105,7 @@ export default class ModelConversionDispatcher { * @param {module:engine/model/model~Model} model Data model. * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls. */ - constructor( model, view, conversionApi = {} ) { + constructor( model, conversionApi = {} ) { /** * Data model instance bound with this dispatcher. * @@ -114,8 +114,6 @@ export default class ModelConversionDispatcher { */ this._model = model; - this._view = view; - /** * Interface passed by dispatcher to the events callbacks. * @@ -129,32 +127,30 @@ export default class ModelConversionDispatcher { * * @param {module:engine/model/differ~Differ} differ Differ object with buffered changes. */ - convertChanges( differ ) { - this._view.change( writer => { - this.conversionApi.writer = writer; + convertChanges( differ, writer ) { + this.conversionApi.writer = writer; - // First, before changing view structure, remove all markers that has changed. - for ( const change of differ.getMarkersToRemove() ) { - this.convertMarkerRemove( change.name, change.range ); - } + // First, before changing view structure, remove all markers that has changed. + for ( const change of differ.getMarkersToRemove() ) { + this.convertMarkerRemove( change.name, change.range, writer ); + } - // Convert changes that happened on model tree. - for ( const entry of differ.getChanges() ) { - if ( entry.type == 'insert' ) { - this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ) ); - } else if ( entry.type == 'remove' ) { - this.convertRemove( entry.position, entry.length, entry.name ); - } else { - // entry.type == 'attribute'. - this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue ); - } + // Convert changes that happened on model tree. + for ( const entry of differ.getChanges() ) { + if ( entry.type == 'insert' ) { + this.convertInsert( Range.createFromPositionAndShift( entry.position, entry.length ), writer ); + } else if ( entry.type == 'remove' ) { + this.convertRemove( entry.position, entry.length, entry.name, writer ); + } else { + // entry.type == 'attribute'. + this.convertAttribute( entry.range, entry.attributeKey, entry.attributeOldValue, entry.attributeNewValue, writer ); } + } - // After the view is updated, convert markers which has changed. - for ( const change of differ.getMarkersToAdd() ) { - this.convertMarkerAdd( change.name, change.range ); - } - } ); + // After the view is updated, convert markers which has changed. + for ( const change of differ.getMarkersToAdd() ) { + this.convertMarkerAdd( change.name, change.range, writer ); + } } /** @@ -167,36 +163,34 @@ export default class ModelConversionDispatcher { * @fires attribute * @param {module:engine/model/range~Range} range Inserted range. */ - convertInsert( range ) { - this._view.change( writer => { - this.conversionApi.writer = writer; - - // Create a list of things that can be consumed, consisting of nodes and their attributes. - const consumable = this._createInsertConsumable( range ); - - // Fire a separate insert event for each node and text fragment contained in the range. - for ( const value of range ) { - const item = value.item; - const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); - const data = { - item, - range: itemRange - }; - - this._testAndFire( 'insert', data, consumable ); - - // Fire a separate addAttribute event for each attribute that was set on inserted items. - // This is important because most attributes converters will listen only to add/change/removeAttribute events. - // If we would not add this part, attributes on inserted nodes would not be converted. - for ( const key of item.getAttributeKeys() ) { - data.attributeKey = key; - data.attributeOldValue = null; - data.attributeNewValue = item.getAttribute( key ); - - this._testAndFire( `attribute:${ key }`, data, consumable ); - } + convertInsert( range, writer ) { + this.conversionApi.writer = writer; + + // Create a list of things that can be consumed, consisting of nodes and their attributes. + const consumable = this._createInsertConsumable( range ); + + // Fire a separate insert event for each node and text fragment contained in the range. + for ( const value of range ) { + const item = value.item; + const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); + const data = { + item, + range: itemRange + }; + + this._testAndFire( 'insert', data, consumable ); + + // Fire a separate addAttribute event for each attribute that was set on inserted items. + // This is important because most attributes converters will listen only to add/change/removeAttribute events. + // If we would not add this part, attributes on inserted nodes would not be converted. + for ( const key of item.getAttributeKeys() ) { + data.attributeKey = key; + data.attributeOldValue = null; + data.attributeNewValue = item.getAttribute( key ); + + this._testAndFire( `attribute:${ key }`, data, consumable ); } - } ); + } } /** @@ -206,12 +200,10 @@ export default class ModelConversionDispatcher { * @param {Number} length Offset size of removed node. * @param {String} name Name of removed node. */ - convertRemove( position, length, name ) { - this._view.change( writer => { - this.conversionApi.writer = writer; + convertRemove( position, length, name, writer ) { + this.conversionApi.writer = writer; - this.fire( 'remove:' + name, { position, length }, this.conversionApi ); - } ); + this.fire( 'remove:' + name, { position, length }, this.conversionApi ); } /** @@ -225,28 +217,26 @@ export default class ModelConversionDispatcher { * @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. */ - convertAttribute( range, key, oldValue, newValue ) { - this._view.change( writer => { - this.conversionApi.writer = writer; - - // Create a list with attributes to consume. - const consumable = this._createConsumableForRange( range, `attribute:${ key }` ); - - // Create a separate attribute event for each node in the range. - for ( const value of range ) { - const item = value.item; - const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); - const data = { - item, - range: itemRange, - attributeKey: key, - attributeOldValue: oldValue, - attributeNewValue: newValue - }; + convertAttribute( range, key, oldValue, newValue, writer ) { + this.conversionApi.writer = writer; - this._testAndFire( `attribute:${ key }`, data, consumable ); - } - } ); + // Create a list with attributes to consume. + const consumable = this._createConsumableForRange( range, `attribute:${ key }` ); + + // Create a separate attribute event for each node in the range. + for ( const value of range ) { + const item = value.item; + const itemRange = Range.createFromPositionAndShift( value.previousPosition, value.length ); + const data = { + item, + range: itemRange, + attributeKey: key, + attributeOldValue: oldValue, + attributeNewValue: newValue + }; + + this._testAndFire( `attribute:${ key }`, data, consumable ); + } } /** @@ -259,45 +249,43 @@ export default class ModelConversionDispatcher { * @fires selectionAttribute * @param {module:engine/model/selection~Selection} selection Selection to convert. */ - convertSelection( selection ) { - this._view.change( writer => { - this.conversionApi.writer = writer; - const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) ); - const consumable = this._createSelectionConsumable( selection, markers ); - - this.fire( 'selection', { selection }, consumable, this.conversionApi ); - - for ( const marker of markers ) { - const markerRange = marker.getRange(); - - if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) { - continue; - } - - const data = { - selection, - markerName: marker.name, - markerRange - }; - - if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) { - this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi ); - } + convertSelection( selection, writer ) { + this.conversionApi.writer = writer; + const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) ); + const consumable = this._createSelectionConsumable( selection, markers ); + + this.fire( 'selection', { selection }, consumable, this.conversionApi ); + + for ( const marker of markers ) { + const markerRange = marker.getRange(); + + if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) { + continue; } - for ( const key of selection.getAttributeKeys() ) { - const data = { - selection, - key, - value: selection.getAttribute( key ) - }; - - // Do not fire event if the attribute has been consumed. - if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) { - this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi ); - } + const data = { + selection, + markerName: marker.name, + markerRange + }; + + if ( consumable.test( selection, 'selectionMarker:' + marker.name ) ) { + this.fire( 'selectionMarker:' + marker.name, data, consumable, this.conversionApi ); } - } ); + } + + for ( const key of selection.getAttributeKeys() ) { + const data = { + selection, + key, + value: selection.getAttribute( key ) + }; + + // Do not fire event if the attribute has been consumed. + if ( consumable.test( selection, 'selectionAttribute:' + data.key ) ) { + this.fire( 'selectionAttribute:' + data.key, data, consumable, this.conversionApi ); + } + } } /** @@ -308,46 +296,44 @@ export default class ModelConversionDispatcher { * @param {String} markerName Marker name. * @param {module:engine/model/range~Range} markerRange Marker range. */ - convertMarkerAdd( markerName, markerRange ) { + convertMarkerAdd( markerName, markerRange, writer ) { // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment). if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) { return; } - this._view.change( writer => { - this.conversionApi.writer = writer; + this.conversionApi.writer = writer; - // In markers' case, event name == consumable name. - const eventName = 'addMarker:' + markerName; + // In markers' case, event name == consumable name. + const eventName = 'addMarker:' + markerName; - // When range is collapsed - fire single event with collapsed range in consumable. - if ( markerRange.isCollapsed ) { - const consumable = new Consumable(); - consumable.add( markerRange, eventName ); + // When range is collapsed - fire single event with collapsed range in consumable. + if ( markerRange.isCollapsed ) { + const consumable = new Consumable(); + consumable.add( markerRange, eventName ); - this.fire( eventName, { - markerName, - markerRange - }, consumable, this.conversionApi ); + this.fire( eventName, { + markerName, + markerRange + }, consumable, this.conversionApi ); - return; - } + return; + } - // Create consumable for each item in range. - const consumable = this._createConsumableForRange( markerRange, eventName ); + // Create consumable for each item in range. + const consumable = this._createConsumableForRange( markerRange, eventName ); - // Create separate event for each node in the range. - for ( const item of markerRange.getItems() ) { - // Do not fire event for already consumed items. - if ( !consumable.test( item, eventName ) ) { - continue; - } + // Create separate event for each node in the range. + for ( const item of markerRange.getItems() ) { + // Do not fire event for already consumed items. + if ( !consumable.test( item, eventName ) ) { + continue; + } - const data = { item, range: Range.createOn( item ), markerName, markerRange }; + const data = { item, range: Range.createOn( item ), markerName, markerRange }; - this.fire( eventName, data, consumable, this.conversionApi ); - } - } ); + this.fire( eventName, data, consumable, this.conversionApi ); + } } /** @@ -357,17 +343,15 @@ export default class ModelConversionDispatcher { * @param {String} markerName Marker name. * @param {module:engine/model/range~Range} markerRange Marker range. */ - convertMarkerRemove( markerName, markerRange ) { + convertMarkerRemove( markerName, markerRange, writer ) { // Do not convert if range is in graveyard or not in the document (e.g. in DocumentFragment). if ( !markerRange.root.document || markerRange.root.rootName == '$graveyard' ) { return; } - this._view.change( writer => { - this.conversionApi.writer = writer; + this.conversionApi.writer = writer; - this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi ); - } ); + this.fire( 'removeMarker:' + markerName, { markerName, markerRange }, this.conversionApi ); } /** diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 5e8181b30..24ce615be 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -20,7 +20,7 @@ import ModelConversionDispatcher from '../conversion/modelconversiondispatcher'; import ModelSelection from '../model/selection'; import ModelDocumentFragment from '../model/documentfragment'; -import View from '../view/view'; +import ViewWriter from '../view/writer'; import ViewConversionDispatcher from '../conversion/viewconversiondispatcher'; import ViewSelection from '../view/selection'; import ViewDocumentFragment from '../view/documentfragment'; @@ -160,7 +160,6 @@ setData._parse = parse; */ export function stringify( node, selectionOrPositionOrRange = null ) { const model = new Model(); - const view = new View(); const mapper = new Mapper(); let selection, range; @@ -194,7 +193,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) { // Setup model to view converter. const viewDocumentFragment = new ViewDocumentFragment(); const viewSelection = new ViewSelection(); - const modelToView = new ModelConversionDispatcher( model, view, { mapper, viewSelection } ); + const modelToView = new ModelConversionDispatcher( model, { mapper, viewSelection } ); // Bind root elements. mapper.bindElements( node.root, viewDocumentFragment ); @@ -218,11 +217,12 @@ export function stringify( node, selectionOrPositionOrRange = null ) { } ) ); // Convert model to view. - modelToView.convertInsert( range ); + const writer = new ViewWriter(); + modelToView.convertInsert( range, writer ); // Convert model selection to view selection. if ( selection ) { - modelToView.convertSelection( selection, [] ); + modelToView.convertSelection( selection, writer ); } // Parse view to data string. diff --git a/tests/conversion/advanced-converters.js b/tests/conversion/advanced-converters.js index b5e444eb8..b2ba7ba3a 100644 --- a/tests/conversion/advanced-converters.js +++ b/tests/conversion/advanced-converters.js @@ -513,7 +513,7 @@ describe( 'advanced-converters', () => { ] ); modelRoot.appendChildren( modelElement ); - modelDispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); + modelDispatcher.convertInsert( ModelRange.createIn( modelRoot ), viewWriter ); expect( viewToString( viewRoot ) ).to.equal( '
' + diff --git a/tests/conversion/model-selection-to-view-converters.js b/tests/conversion/model-selection-to-view-converters.js index 454f1ba60..ab8d9afbf 100644 --- a/tests/conversion/model-selection-to-view-converters.js +++ b/tests/conversion/model-selection-to-view-converters.js @@ -12,7 +12,6 @@ import View from '../../src/view/view'; import ViewContainerElement from '../../src/view/containerelement'; import ViewAttributeElement from '../../src/view/attributeelement'; import ViewUIElement from '../../src/view/uielement'; -import ViewWriter from '../../src/view/writer'; import Mapper from '../../src/conversion/mapper'; import ModelConversionDispatcher from '../../src/conversion/modelconversiondispatcher'; @@ -59,7 +58,7 @@ describe( 'model-selection-to-view-converters', () => { highlightDescriptor = { class: 'marker', priority: 1 }; - dispatcher = new ModelConversionDispatcher( model, view, { mapper, viewSelection } ); + dispatcher = new ModelConversionDispatcher( model, { mapper, viewSelection } ); dispatcher.on( 'insert:$text', insertText() ); dispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) ); @@ -233,11 +232,11 @@ describe( 'model-selection-to-view-converters', () => { viewRoot.removeChildren( 0, viewRoot.childCount ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange() ); - - const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) ); - dispatcher.convertSelection( modelSelection, markers ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( @@ -260,11 +259,11 @@ describe( 'model-selection-to-view-converters', () => { viewRoot.removeChildren( 0, viewRoot.childCount ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange() ); - - const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) ); - dispatcher.convertSelection( modelSelection, markers ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) @@ -285,11 +284,11 @@ describe( 'model-selection-to-view-converters', () => { viewRoot.removeChildren( 0, viewRoot.childCount ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange() ); - - const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) ); - dispatcher.convertSelection( modelSelection, markers ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) @@ -310,11 +309,11 @@ describe( 'model-selection-to-view-converters', () => { viewRoot.removeChildren( 0, viewRoot.childCount ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange() ); - - const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) ); - dispatcher.convertSelection( modelSelection, markers ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) @@ -335,11 +334,11 @@ describe( 'model-selection-to-view-converters', () => { viewRoot.removeChildren( 0, viewRoot.childCount ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange() ); - - const markers = Array.from( model.markers.getMarkersAtPosition( modelSelection.getFirstPosition() ) ); - dispatcher.convertSelection( modelSelection, markers ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) @@ -360,7 +359,9 @@ describe( 'model-selection-to-view-converters', () => { modelSelection.setAttribute( 'bold', true ); // Convert model to view. - dispatcher.convertSelection( modelSelection, [] ); + view.change( writer => { + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) @@ -375,13 +376,15 @@ describe( 'model-selection-to-view-converters', () => { modelSelection.setAttribute( 'bold', true ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); - // Add ui element to view. - const uiElement = new ViewUIElement( 'span' ); - viewRoot.insertChildren( 1, uiElement ); + // Add ui element to view. + const uiElement = new ViewUIElement( 'span' ); + viewRoot.insertChildren( 1, uiElement ); - dispatcher.convertSelection( modelSelection, [] ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) @@ -396,13 +399,14 @@ describe( 'model-selection-to-view-converters', () => { modelSelection.setAttribute( 'bold', true ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); - - // Add ui element to view. - const uiElement = new ViewUIElement( 'span' ); - viewRoot.insertChildren( 1, uiElement ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); - dispatcher.convertSelection( modelSelection, [] ); + // Add ui element to view. + const uiElement = new ViewUIElement( 'span' ); + viewRoot.insertChildren( 1, uiElement, writer ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) @@ -479,10 +483,12 @@ describe( 'model-selection-to-view-converters', () => { { bold: 'true' } ); - const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 ); - modelDoc.selection.setRanges( [ modelRange ] ); + view.change( writer => { + const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 ); + modelDoc.selection.setRanges( [ modelRange ] ); - dispatcher.convertSelection( modelDoc.selection, [] ); + dispatcher.convertSelection( modelDoc.selection, writer ); + } ); expect( viewSelection.rangeCount ).to.equal( 1 ); @@ -501,14 +507,15 @@ describe( 'model-selection-to-view-converters', () => { { bold: 'true' } ); - // Remove manually. - const viewWriter = new ViewWriter(); - viewWriter.mergeAttributes( viewSelection.getFirstPosition() ); + view.change( writer => { + // Remove manually. + writer.mergeAttributes( viewSelection.getFirstPosition() ); - const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 ); - modelDoc.selection.setRanges( [ modelRange ] ); + const modelRange = ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 ); + modelDoc.selection.setRanges( [ modelRange ] ); - dispatcher.convertSelection( modelDoc.selection, [] ); + dispatcher.convertSelection( modelDoc.selection, writer ); + } ); expect( viewSelection.rangeCount ).to.equal( 1 ); @@ -520,10 +527,11 @@ describe( 'model-selection-to-view-converters', () => { describe( 'clearFakeSelection', () => { it( 'should clear fake selection', () => { dispatcher.on( 'selection', clearFakeSelection() ); - viewSelection.setFake( true ); - - dispatcher.convertSelection( modelSelection, [] ); + view.change( writer => { + viewSelection.setFake( true ); + dispatcher.convertSelection( modelSelection, writer ); + } ); expect( viewSelection.isFake ).to.be.false; } ); } ); @@ -740,8 +748,10 @@ describe( 'model-selection-to-view-converters', () => { viewRoot.removeChildren( 0, viewRoot.childCount ); // Convert model to view. - dispatcher.convertInsert( ModelRange.createIn( modelRoot ) ); - dispatcher.convertSelection( modelSelection, [] ); + view.change( writer => { + dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); + dispatcher.convertSelection( modelSelection, writer ); + } ); // Stringify view and check if it is same as expected. expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '
' + expectedView + '
' ); diff --git a/tests/conversion/modelconversiondispatcher.js b/tests/conversion/modelconversiondispatcher.js index fb06f527e..44427fb11 100644 --- a/tests/conversion/modelconversiondispatcher.js +++ b/tests/conversion/modelconversiondispatcher.js @@ -20,7 +20,7 @@ describe( 'ModelConversionDispatcher', () => { model = new Model(); view = new View(); doc = model.document; - dispatcher = new ModelConversionDispatcher( model, view ); + dispatcher = new ModelConversionDispatcher( model ); root = doc.createRoot(); differStub = { @@ -33,7 +33,7 @@ describe( 'ModelConversionDispatcher', () => { describe( 'constructor()', () => { it( 'should create ModelConversionDispatcher with given api', () => { const apiObj = {}; - const dispatcher = new ModelConversionDispatcher( model, view, { apiObj } ); + const dispatcher = new ModelConversionDispatcher( model, { apiObj } ); expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj ); } ); @@ -48,7 +48,9 @@ describe( 'ModelConversionDispatcher', () => { differStub.getChanges = () => [ { type: 'insert', position, length: 1 } ]; - dispatcher.convertChanges( differStub ); + view.change( writer => { + dispatcher.convertChanges( differStub, writer ); + } ); expect( dispatcher.convertInsert.calledOnce ).to.be.true; expect( dispatcher.convertInsert.firstCall.args[ 0 ].isEqual( range ) ).to.be.true; @@ -61,7 +63,9 @@ describe( 'ModelConversionDispatcher', () => { differStub.getChanges = () => [ { type: 'remove', position, length: 2, name: '$text' } ]; - dispatcher.convertChanges( differStub ); + view.change( writer => { + dispatcher.convertChanges( differStub, writer ); + } ); expect( dispatcher.convertRemove.calledWith( position, 2, '$text' ) ).to.be.true; } ); @@ -76,7 +80,9 @@ describe( 'ModelConversionDispatcher', () => { { type: 'attribute', position, range, attributeKey: 'key', attributeOldValue: null, attributeNewValue: 'foo' } ]; - dispatcher.convertChanges( differStub ); + view.change( writer => { + dispatcher.convertChanges( differStub, writer ); + } ); expect( dispatcher.convertAttribute.calledWith( range, 'key', null, 'foo' ) ).to.be.true; } ); @@ -96,7 +102,9 @@ describe( 'ModelConversionDispatcher', () => { { type: 'insert', position, length: 3 }, ]; - dispatcher.convertChanges( differStub ); + view.change( writer => { + dispatcher.convertChanges( differStub, writer ); + } ); expect( dispatcher.convertInsert.calledTwice ).to.be.true; expect( dispatcher.convertRemove.calledOnce ).to.be.true; @@ -114,7 +122,9 @@ describe( 'ModelConversionDispatcher', () => { { name: 'bar', range: barRange } ]; - dispatcher.convertChanges( differStub ); + view.change( writer => { + dispatcher.convertChanges( differStub, writer ); + } ); expect( dispatcher.convertMarkerAdd.calledWith( 'foo', fooRange ) ); expect( dispatcher.convertMarkerAdd.calledWith( 'bar', barRange ) ); @@ -131,7 +141,9 @@ describe( 'ModelConversionDispatcher', () => { { name: 'bar', range: barRange } ]; - dispatcher.convertChanges( differStub ); + view.change( writer => { + dispatcher.convertChanges( differStub, writer ); + } ); expect( dispatcher.convertMarkerRemove.calledWith( 'foo', fooRange ) ); expect( dispatcher.convertMarkerRemove.calledWith( 'bar', barRange ) ); From 7eaa1baea33f81c35b5c61a6e06bba84338a81c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 22 Jan 2018 10:20:10 +0100 Subject: [PATCH 20/89] Moving model to view selection conversion into view.change block. --- src/controller/editingcontroller.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index 137316634..0ddbd046a 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -88,11 +88,6 @@ export default class EditingController { this.listenTo( doc, 'change', () => { this.view.change( writer => { this.modelToView.convertChanges( doc.differ, writer ); - } ); - }, { priority: 'low' } ); - - this.listenTo( model, '_change', () => { - this.view.change( writer => { this.modelToView.convertSelection( doc.selection, writer ); } ); }, { priority: 'low' } ); From 1737084acadc0131b3e945de47ac24949ed3a97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 22 Jan 2018 14:29:32 +0100 Subject: [PATCH 21/89] Changed view utils methods to accept view instance instead of view document. --- src/dev-utils/view.js | 20 +++--- tests/controller/editingcontroller.js | 68 +++++++++---------- .../view-selection-to-model-converters.js | 4 +- tests/dev-utils/view.js | 20 +++--- tests/tickets/699.js | 2 +- tests/view/observer/fakeselectionobserver.js | 2 +- tests/view/observer/focusobserver.js | 4 +- tests/view/placeholder.js | 26 +++---- tests/view/renderer.js | 20 +++--- tests/view/view/jumpoverinlinefiller.js | 10 +-- tests/view/view/jumpoveruielement.js | 16 ++--- 11 files changed, 97 insertions(+), 95 deletions(-) diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js index 58444bf34..bfebd5e81 100644 --- a/src/dev-utils/view.js +++ b/src/dev-utils/view.js @@ -11,7 +11,7 @@ * Collection of methods for manipulating {@link module:engine/view/view view} for testing purposes. */ -import Document from '../view/document'; +import View from '../view/view'; import ViewDocumentFragment from '../view/documentfragment'; import XmlDataProcessor from '../dataprocessor/xmldataprocessor'; import ViewElement from '../view/element'; @@ -37,7 +37,7 @@ const allowedTypes = { /** * Writes the contents of the {@link module:engine/view/document~Document Document} to an HTML-like string. * - * @param {module:engine/view/document~Document} document + * @param {module:engine/view/view~View} view * @param {Object} [options] * @param {Boolean} [options.withoutSelection=false] Whether to write the selection. When set to `true` selection will * be not included in returned string. @@ -49,11 +49,12 @@ const allowedTypes = { * (``, ``). * @returns {String} The stringified data. */ -export function getData( document, options = {} ) { - if ( !( document instanceof Document ) ) { - throw new TypeError( 'Document needs to be an instance of module:engine/view/document~Document.' ); +export function getData( view, options = {} ) { + if ( !( view instanceof View ) ) { + throw new TypeError( 'View needs to be an instance of module:engine/view/view~View.' ); } + const document = view.document; const withoutSelection = !!options.withoutSelection; const rootName = options.rootName || 'main'; const root = document.getRoot( rootName ); @@ -74,17 +75,18 @@ getData._stringify = stringify; /** * Sets the contents of the {@link module:engine/view/document~Document Document} provided as HTML-like string. * - * @param {module:engine/view/document~Document} document + * @param {module:engine/view/view~View} view * @param {String} data HTML-like string to write into Document. * @param {Object} options * @param {String} [options.rootName='main'] Root name where parsed data will be stored. If not provided, * default `main` name will be used. */ -export function setData( document, data, options = {} ) { - if ( !( document instanceof Document ) ) { - throw new TypeError( 'Document needs to be an instance of module:engine/view/document~Document.' ); +export function setData( view, data, options = {} ) { + if ( !( view instanceof View ) ) { + throw new TypeError( 'View needs to be an instance of module:engine/view/view~View.' ); } + const document = view.document; const rootName = options.rootName || 'main'; const root = document.getRoot( rootName ); const result = setData._parse( data, { rootElement: root } ); diff --git a/tests/controller/editingcontroller.js b/tests/controller/editingcontroller.js index d7e10a0d3..e1e20a850 100644 --- a/tests/controller/editingcontroller.js +++ b/tests/controller/editingcontroller.js @@ -122,11 +122,11 @@ describe( 'EditingController', () => { } ); it( 'should convert insertion', () => { - expect( getViewData( editing.view.document ) ).to.equal( '

f{}oo

bar

' ); + expect( getViewData( editing.view ) ).to.equal( '

f{}oo

bar

' ); } ); it( 'should convert split', () => { - expect( getViewData( editing.view.document ) ).to.equal( '

f{}oo

bar

' ); + expect( getViewData( editing.view ) ).to.equal( '

f{}oo

bar

' ); model.change( writer => { writer.split( model.document.selection.getFirstPosition() ); @@ -136,17 +136,17 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view.document ) ).to.equal( '

f

{}oo

bar

' ); + expect( getViewData( editing.view ) ).to.equal( '

f

{}oo

bar

' ); } ); it( 'should convert rename', () => { - expect( getViewData( editing.view.document ) ).to.equal( '

f{}oo

bar

' ); + expect( getViewData( editing.view ) ).to.equal( '

f{}oo

bar

' ); model.change( writer => { writer.rename( modelRoot.getChild( 0 ), 'div' ); } ); - expect( getViewData( editing.view.document ) ).to.equal( '
f{}oo

bar

' ); + expect( getViewData( editing.view ) ).to.equal( '
f{}oo

bar

' ); } ); it( 'should convert delete', () => { @@ -160,7 +160,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view.document ) ).to.equal( '

f{}o

bar

' ); + expect( getViewData( editing.view ) ).to.equal( '

f{}o

bar

' ); } ); it( 'should convert selection from view to model', done => { @@ -195,7 +195,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view.document ) ).to.equal( '

foo

b{}ar

' ); + expect( getViewData( editing.view ) ).to.equal( '

foo

b{}ar

' ); } ); it( 'should convert not collapsed selection', () => { @@ -205,7 +205,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view.document ) ).to.equal( '

foo

b{a}r

' ); + expect( getViewData( editing.view ) ).to.equal( '

foo

b{a}r

' ); } ); it( 'should clear previous selection', () => { @@ -215,7 +215,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view.document ) ).to.equal( '

foo

b{}ar

' ); + expect( getViewData( editing.view ) ).to.equal( '

foo

b{}ar

' ); model.change( () => { model.document.selection.setRanges( [ @@ -223,7 +223,7 @@ describe( 'EditingController', () => { ] ); } ); - expect( getViewData( editing.view.document ) ).to.equal( '

foo

ba{}r

' ); + expect( getViewData( editing.view ) ).to.equal( '

foo

ba{}r

' ); } ); it( 'should convert adding marker', () => { @@ -233,7 +233,7 @@ describe( 'EditingController', () => { model.markers.set( 'marker', range ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ) + expect( getViewData( editing.view, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); @@ -248,7 +248,7 @@ describe( 'EditingController', () => { model.markers.remove( 'marker' ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ) + expect( getViewData( editing.view, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); @@ -265,7 +265,7 @@ describe( 'EditingController', () => { model.markers.set( 'marker', range2 ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ) + expect( getViewData( editing.view, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); @@ -280,7 +280,7 @@ describe( 'EditingController', () => { writer.insertText( 'xyz', new ModelPosition( modelRoot, [ 1, 0 ] ) ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ) + expect( getViewData( editing.view, { withoutSelection: true } ) ) .to.equal( '

foo

xyz

bar

' ); } ); @@ -298,7 +298,7 @@ describe( 'EditingController', () => { ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ) + expect( getViewData( editing.view, { withoutSelection: true } ) ) .to.equal( '

foor

ba

' ); } ); @@ -316,7 +316,7 @@ describe( 'EditingController', () => { ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ) + expect( getViewData( editing.view, { withoutSelection: true } ) ) .to.equal( '

f

baroo

' ); } ); @@ -334,7 +334,7 @@ describe( 'EditingController', () => { ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ) + expect( getViewData( editing.view, { withoutSelection: true } ) ) .to.equal( '

foo

bar

' ); } ); } ); @@ -389,14 +389,14 @@ describe( 'EditingController', () => { // Adding with 'high' priority, because `applyOperation` is decorated - its default callback is fired with 'normal' priority. model.on( 'applyOperation', () => { expect( mcd.convertMarkerRemove.calledOnce ).to.be.true; - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); }, { priority: 'high' } ); model.change( writer => { writer.insertText( 'a', p1, 0 ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

afoo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

afoo

bar

' ); } ); it( 'should remove marker from view if it will be affected by remove operation', () => { @@ -407,14 +407,14 @@ describe( 'EditingController', () => { // Adding with 'high' priority, because `applyOperation` is decorated - its default callback is fired with 'normal' priority. model.on( 'applyOperation', () => { expect( mcd.convertMarkerRemove.calledOnce ).to.be.true; - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); }, { priority: 'high' } ); model.change( writer => { writer.remove( ModelRange.createFromParentsAndOffsets( p1, 0, p1, 1 ) ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

oo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

oo

bar

' ); } ); it( 'should remove marker from view if it will be affected by move operation', () => { @@ -425,7 +425,7 @@ describe( 'EditingController', () => { // Adding with 'high' priority, because `applyOperation` is decorated - its default callback is fired with 'normal' priority. model.on( 'applyOperation', () => { expect( mcd.convertMarkerRemove.calledOnce ).to.be.true; - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); }, { priority: 'high' } ); model.change( writer => { @@ -434,7 +434,7 @@ describe( 'EditingController', () => { writer.move( ModelRange.createFromParentsAndOffsets( p2, 0, p2, 2 ), p1, 0 ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

bafoo

r

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

bafoo

r

' ); } ); it( 'should remove marker from view if it will be affected by rename operation', () => { @@ -445,14 +445,14 @@ describe( 'EditingController', () => { // Adding with 'high' priority, because `applyOperation` is decorated - its default callback is fired with 'normal' priority. model.on( 'applyOperation', () => { expect( mcd.convertMarkerRemove.calledOnce ).to.be.true; - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); }, { priority: 'high' } ); model.change( writer => { writer.rename( p1, 'div' ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '
foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '
foo

bar

' ); } ); it( 'should remove marker from view if it will be affected by marker operation', () => { @@ -463,7 +463,7 @@ describe( 'EditingController', () => { // Adding with 'high' priority, because `applyOperation` is decorated - its default callback is fired with 'normal' priority. model.on( 'applyOperation', () => { expect( mcd.convertMarkerRemove.calledOnce ).to.be.true; - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); }, { priority: 'high' } ); model.change( writer => { @@ -472,7 +472,7 @@ describe( 'EditingController', () => { writer.setMarker( 'marker', ModelRange.createFromParentsAndOffsets( p2, 1, p2, 2 ) ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); } ); it( 'should remove marker from view if it is removed through marker collection', () => { @@ -483,14 +483,14 @@ describe( 'EditingController', () => { // Adding with 'high' priority, because `applyOperation` is decorated - its default callback is fired with 'normal' priority. model.markers.on( 'remove:marker', () => { expect( mcd.convertMarkerRemove.calledOnce ).to.be.true; - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); }, { priority: 'low' } ); model.change( () => { model.markers.remove( 'marker' ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); } ); it( 'should not remove marker if applied operation is an attribute operation', () => { @@ -501,14 +501,14 @@ describe( 'EditingController', () => { // Adding with 'high' priority, because `applyOperation` is decorated - its default callback is fired with 'normal' priority. model.on( 'applyOperation', () => { expect( mcd.convertMarkerRemove.calledOnce ).to.be.false; - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); }, { priority: 'high' } ); model.change( writer => { writer.setAttribute( 'foo', 'bar', ModelRange.createFromParentsAndOffsets( p1, 0, p1, 2 ) ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); } ); it( 'should not crash if multiple operations affect a marker', () => { @@ -522,7 +522,7 @@ describe( 'EditingController', () => { writer.insertText( 'a', p1, 0 ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

aaafoo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

aaafoo

bar

' ); } ); it( 'should not crash if marker is removed, added and removed #1', () => { @@ -536,7 +536,7 @@ describe( 'EditingController', () => { writer.insertText( 'a', p1, 0 ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

aafoo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

aafoo

bar

' ); } ); it( 'should not crash if marker is removed, added and removed #2', () => { @@ -550,7 +550,7 @@ describe( 'EditingController', () => { writer.removeMarker( 'marker' ); } ); - expect( getViewData( editing.view.document, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); + expect( getViewData( editing.view, { withoutSelection: true } ) ).to.equal( '

foo

bar

' ); } ); } ); diff --git a/tests/conversion/view-selection-to-model-converters.js b/tests/conversion/view-selection-to-model-converters.js index fbcadd911..9d4a5f47f 100644 --- a/tests/conversion/view-selection-to-model-converters.js +++ b/tests/conversion/view-selection-to-model-converters.js @@ -30,7 +30,7 @@ describe( 'convertSelectionChange', () => { viewDocument = view.document; viewRoot = createViewRoot( viewDocument, 'div', 'main' ); - viewSetData( viewDocument, '

foo

bar

' ); + viewSetData( view, '

foo

bar

' ); mapper = new Mapper(); mapper.bindElements( modelRoot, viewRoot ); @@ -57,7 +57,7 @@ describe( 'convertSelectionChange', () => { it( 'should support unicode', () => { modelSetData( model, 'நிலைக்கு' ); - viewSetData( viewDocument, '

நிலைக்கு

' ); + viewSetData( view, '

நிலைக்கு

' ); // Re-bind elements that were just re-set. mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); diff --git a/tests/dev-utils/view.js b/tests/dev-utils/view.js index ce0ce493c..7cce7b427 100644 --- a/tests/dev-utils/view.js +++ b/tests/dev-utils/view.js @@ -42,7 +42,7 @@ describe( 'view test utils', () => { const root = createAttachedRoot( viewDocument, element ); root.appendChildren( new Element( 'p' ) ); - expect( getData( viewDocument, options ) ).to.equal( '

' ); + expect( getData( view, options ) ).to.equal( '

' ); sinon.assert.calledOnce( stringifySpy ); expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root ); expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( null ); @@ -65,7 +65,7 @@ describe( 'view test utils', () => { viewDocument.selection.addRange( Range.createFromParentsAndOffsets( root, 0, root, 1 ) ); - expect( getData( viewDocument, options ) ).to.equal( '[

]' ); + expect( getData( view, options ) ).to.equal( '[

]' ); sinon.assert.calledOnce( stringifySpy ); expect( stringifySpy.firstCall.args[ 0 ] ).to.equal( root ); expect( stringifySpy.firstCall.args[ 1 ] ).to.equal( viewDocument.selection ); @@ -79,8 +79,8 @@ describe( 'view test utils', () => { it( 'should throw an error when passing invalid document', () => { expect( () => { - getData( { invalid: 'document' } ); - } ).to.throw( TypeError, 'Document needs to be an instance of module:engine/view/document~Document.' ); + getData( { invalid: 'view' } ); + } ).to.throw( TypeError, 'View needs to be an instance of module:engine/view/view~View.' ); } ); } ); @@ -92,9 +92,9 @@ describe( 'view test utils', () => { const parseSpy = sandbox.spy( setData, '_parse' ); createAttachedRoot( viewDocument, document.createElement( 'div' ) ); - setData( viewDocument, data ); + setData( view, data ); - expect( getData( viewDocument ) ).to.equal( 'foobarbaz' ); + expect( getData( view ) ).to.equal( 'foobarbaz' ); sinon.assert.calledOnce( parseSpy ); const args = parseSpy.firstCall.args; expect( args[ 0 ] ).to.equal( data ); @@ -111,9 +111,9 @@ describe( 'view test utils', () => { const parseSpy = sandbox.spy( setData, '_parse' ); createAttachedRoot( viewDocument, document.createElement( 'div' ) ); - setData( viewDocument, data ); + setData( view, data ); - expect( getData( viewDocument ) ).to.equal( '[baz]' ); + expect( getData( view ) ).to.equal( '[baz]' ); const args = parseSpy.firstCall.args; expect( args[ 0 ] ).to.equal( data ); expect( args[ 1 ] ).to.be.an( 'object' ); @@ -124,8 +124,8 @@ describe( 'view test utils', () => { it( 'should throw an error when passing invalid document', () => { expect( () => { - setData( { invalid: 'document' } ); - } ).to.throw( TypeError, 'Document needs to be an instance of module:engine/view/document~Document.' ); + setData( { invalid: 'view' } ); + } ).to.throw( TypeError, 'View needs to be an instance of module:engine/view/view~View.' ); } ); } ); } ); diff --git a/tests/tickets/699.js b/tests/tickets/699.js index 12623977a..87ca47eba 100644 --- a/tests/tickets/699.js +++ b/tests/tickets/699.js @@ -34,7 +34,7 @@ describe( 'Bug ckeditor5-engine#699', () => { editor.setData( '

foo

' ); expect( getModelData( editor.model ) ).to.equal( '[]foo' ); - expect( getViewData( editor.editing.view.document ) ).to.equal( '[]

foo

' ); + expect( getViewData( editor.editing.view ) ).to.equal( '[]

foo

' ); return editor.destroy(); } ); diff --git a/tests/view/observer/fakeselectionobserver.js b/tests/view/observer/fakeselectionobserver.js index 85cbaa658..cd8dba200 100644 --- a/tests/view/observer/fakeselectionobserver.js +++ b/tests/view/observer/fakeselectionobserver.js @@ -191,7 +191,7 @@ describe( 'FakeSelectionObserver', () => { resolve(); } ); - setData( viewDocument, initialData ); + setData( view, initialData ); changeFakeSelectionPressing( keyCode ); } ); } diff --git a/tests/view/observer/focusobserver.js b/tests/view/observer/focusobserver.js index 0a3b4d621..2d2c13419 100644 --- a/tests/view/observer/focusobserver.js +++ b/tests/view/observer/focusobserver.js @@ -163,7 +163,7 @@ describe( 'FocusObserver', () => { const selectionChangeSpy = sinon.spy(); const renderSpy = sinon.spy(); - setData( viewDocument, '
foo bar
' ); + setData( view, '
foo bar
' ); view.render(); viewDocument.on( 'selectionChange', selectionChangeSpy ); @@ -184,7 +184,7 @@ describe( 'FocusObserver', () => { const selectionChangeSpy = sinon.spy(); const renderSpy = sinon.spy(); - setData( viewDocument, '
foo bar
' ); + setData( view, '
foo bar
' ); view.render(); const domEditable = domRoot.childNodes[ 0 ]; diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js index 194745f6d..f7b273fc4 100644 --- a/tests/view/placeholder.js +++ b/tests/view/placeholder.js @@ -30,7 +30,7 @@ describe( 'placeholder', () => { } ); it( 'should attach proper CSS class and data attribute', () => { - setData( viewDocument, '
{another div}
' ); + setData( view, '
{another div}
' ); 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, '
{another div}
' ); + setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); const secondView = new View(); const secondDocument = secondView.document; secondDocument.isFocused = true; const secondRoot = createViewRoot( secondDocument ); - setData( secondDocument, '
{another div}
' ); + setData( secondView, '
{another div}
' ); const secondElement = secondRoot.getChild( 0 ); attachPlaceholder( element, 'first placeholder' ); @@ -168,7 +168,7 @@ describe( 'placeholder', () => { describe( 'detachPlaceholder', () => { it( 'should remove placeholder from element', () => { - setData( viewDocument, '
{another div}
' ); + setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); attachPlaceholder( element, 'foo bar baz' ); diff --git a/tests/view/renderer.js b/tests/view/renderer.js index 65bcfe925..8b79f3181 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -1889,7 +1889,7 @@ describe( 'Renderer', () => { } ); it( 'should properly render unwrapped attributes #1', () => { - setViewData( viewDoc, + setViewData( view, '' + '[' + 'f' + @@ -1903,7 +1903,7 @@ describe( 'Renderer', () => { // Unwrap italic attribute element. writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); - expect( getViewData( viewDoc ) ).to.equal( '

[foo]

' ); + expect( getViewData( view ) ).to.equal( '

[foo]

' ); // Re-render changes in view to DOM. view.render(); @@ -1914,7 +1914,7 @@ describe( 'Renderer', () => { } ); it( 'should properly render unwrapped attributes #2', () => { - setViewData( viewDoc, + setViewData( view, '' + '[' + 'foo' + @@ -1927,7 +1927,7 @@ describe( 'Renderer', () => { // Unwrap italic attribute element and change text inside. writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); viewRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ).data = 'bar'; - expect( getViewData( viewDoc ) ).to.equal( '

[bar]

' ); + expect( getViewData( view ) ).to.equal( '

[bar]

' ); // 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( '
' + diff --git a/tests/manual/placeholder.js b/tests/manual/placeholder.js index e89e47e82..91121fbad 100644 --- a/tests/manual/placeholder.js +++ b/tests/manual/placeholder.js @@ -20,13 +20,14 @@ ClassicEditor toolbar: [ 'headings', 'undo', 'redo' ] } ) .then( editor => { - const viewDoc = editor.editing.view; + const view = editor.editing.view; + const viewDoc = view.document; const header = viewDoc.getRoot().getChild( 0 ); const paragraph = viewDoc.getRoot().getChild( 1 ); - attachPlaceholder( header, 'Type some header text...' ); - attachPlaceholder( paragraph, 'Type some paragraph text...' ); - viewDoc.render(); + attachPlaceholder( view, header, 'Type some header text...' ); + attachPlaceholder( view, paragraph, 'Type some paragraph text...' ); + view.render(); } ) .catch( err => { console.error( err.stack ); diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js index f7b273fc4..047bc1d71 100644 --- a/tests/view/placeholder.js +++ b/tests/view/placeholder.js @@ -5,7 +5,6 @@ import { attachPlaceholder, detachPlaceholder } from '../../src/view/placeholder'; import createViewRoot from './_utils/createroot'; -import ViewContainerElement from '../../src/view/containerelement'; import View from '../../src/view/view'; import ViewRange from '../../src/view/range'; import { setData } from '../../src/dev-utils/view'; @@ -21,19 +20,11 @@ describe( 'placeholder', () => { } ); describe( 'createPlaceholder', () => { - it( 'should throw if element is not inside document', () => { - const element = new ViewContainerElement( 'div' ); - - expect( () => { - attachPlaceholder( element, 'foo bar baz' ); - } ).to.throw( 'view-placeholder-element-is-detached' ); - } ); - it( 'should attach proper CSS class and data attribute', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; @@ -43,7 +34,7 @@ describe( 'placeholder', () => { setData( view, '
first div
{another div}
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; @@ -53,7 +44,7 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; @@ -63,7 +54,7 @@ describe( 'placeholder', () => { setData( view, '
[]
another div
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; @@ -74,7 +65,7 @@ describe( 'placeholder', () => { const element = viewRoot.getChild( 0 ); viewDocument.isFocused = false; - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; @@ -85,7 +76,7 @@ describe( 'placeholder', () => { const element = viewRoot.getChild( 0 ); const spy = sinon.spy( () => false ); - attachPlaceholder( element, 'foo bar baz', spy ); + attachPlaceholder( view, element, 'foo bar baz', spy ); sinon.assert.calledOnce( spy ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); @@ -96,7 +87,7 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; @@ -112,8 +103,8 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); - attachPlaceholder( element, 'new text' ); + attachPlaceholder( view, element, 'foo bar baz' ); + attachPlaceholder( view, element, 'new text' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'new text' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; @@ -123,7 +114,7 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); setData( view, '

paragraph

' ); view.render(); @@ -140,8 +131,8 @@ describe( 'placeholder', () => { setData( secondView, '
{another div}
' ); const secondElement = secondRoot.getChild( 0 ); - attachPlaceholder( element, 'first placeholder' ); - attachPlaceholder( secondElement, 'second placeholder' ); + attachPlaceholder( view, element, 'first placeholder' ); + attachPlaceholder( secondView, secondElement, 'second placeholder' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; @@ -171,7 +162,7 @@ describe( 'placeholder', () => { setData( view, '
{another div}
' ); const element = viewRoot.getChild( 0 ); - attachPlaceholder( element, 'foo bar baz' ); + attachPlaceholder( view, element, 'foo bar baz' ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; diff --git a/tests/view/renderer.js b/tests/view/renderer.js index 8b79f3181..b1e00ef6b 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -21,16 +21,14 @@ import { INLINE_FILLER, INLINE_FILLER_LENGTH, isBlockFiller, BR_FILLER } from '. import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import createViewRoot from './_utils/createroot'; import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; -import Writer from '../../src/view/writer'; import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml'; testUtils.createSinonSandbox(); describe( 'Renderer', () => { - let selection, domConverter, renderer, writer; + let selection, domConverter, renderer; beforeEach( () => { - writer = new Writer(); selection = new Selection(); domConverter = new DomConverter(); renderer = new Renderer( domConverter, selection ); @@ -1902,7 +1900,10 @@ describe( 'Renderer', () => { view.render(); // Unwrap italic attribute element. - writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); + view.change( writer => { + writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); + } ); + expect( getViewData( view ) ).to.equal( '

[foo]

' ); // Re-render changes in view to DOM. @@ -1925,7 +1926,10 @@ describe( 'Renderer', () => { view.render(); // Unwrap italic attribute element and change text inside. - writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); + view.change( writer => { + writer.unwrap( viewDoc.selection.getFirstRange(), new ViewAttributeElement( 'italic' ) ); + } ); + viewRoot.getChild( 0 ).getChild( 0 ).getChild( 0 ).data = 'bar'; expect( getViewData( view ) ).to.equal( '

[bar]

' ); @@ -1948,7 +1952,11 @@ describe( 'Renderer', () => { // Change text and insert new element into paragraph. const textNode = viewRoot.getChild( 0 ).getChild( 0 ); textNode.data = 'foobar'; - writer.insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) ); + + view.change( writer => { + writer.insert( ViewPosition.createAfter( textNode ), new ViewAttributeElement( 'img' ) ); + } ); + expect( getViewData( view ) ).to.equal( '

foobar

' ); // Re-render changes in view to DOM. @@ -1970,7 +1978,11 @@ describe( 'Renderer', () => { // Change text and insert new element into paragraph. const textNode = viewRoot.getChild( 0 ).getChild( 0 ); textNode.data = 'foobar'; - writer.insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) ); + + view.change( writer => { + writer.insert( ViewPosition.createBefore( textNode ), new ViewAttributeElement( 'img' ) ); + } ); + expect( getViewData( view ) ).to.equal( '

foobar

' ); // Re-render changes in view to DOM. @@ -1997,8 +2009,11 @@ describe( 'Renderer', () => { const container = viewRoot.getChild( 0 ); const firstElement = container.getChild( 0 ); - writer.remove( ViewRange.createOn( firstElement ) ); - writer.insert( new ViewPosition( container, 2 ), firstElement ); + view.change( writer => { + writer.remove( ViewRange.createOn( firstElement ) ); + writer.insert( new ViewPosition( container, 2 ), firstElement ); + } ); + expect( getViewData( view ) ).to.equal( '

' ); // Re-render changes in view to DOM. diff --git a/tests/view/writer/breakattributes.js b/tests/view/writer/breakattributes.js index 72c7db6fd..09e988363 100644 --- a/tests/view/writer/breakattributes.js +++ b/tests/view/writer/breakattributes.js @@ -4,6 +4,7 @@ */ import Writer from '../../../src/view/writer'; +import Document from '../../../src/view/document'; import { stringify, parse } from '../../../src/dev-utils/view'; import ContainerElement from '../../../src/view/containerelement'; import AttributeElement from '../../../src/view/attributeelement'; @@ -18,7 +19,7 @@ describe( 'Writer', () => { let writer; before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); describe( 'break position', () => { diff --git a/tests/view/writer/breakcontainer.js b/tests/view/writer/breakcontainer.js index 7536f7b49..330217b11 100644 --- a/tests/view/writer/breakcontainer.js +++ b/tests/view/writer/breakcontainer.js @@ -8,6 +8,7 @@ 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'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'breakContainer()', () => { @@ -26,7 +27,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'break inside element - should break container element at given position', () => { diff --git a/tests/view/writer/clear.js b/tests/view/writer/clear.js index 793d12a96..0a5e42732 100644 --- a/tests/view/writer/clear.js +++ b/tests/view/writer/clear.js @@ -11,6 +11,7 @@ import AttributeElement from '../../../src/view/attributeelement'; import EmptyElement from '../../../src/view/emptyelement'; import UIElement from '../../../src/view/uielement'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'clear()', () => { @@ -30,7 +31,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'should throw when range placed in two containers', () => { diff --git a/tests/view/writer/insert.js b/tests/view/writer/insert.js index 5f4419a95..7948104cf 100644 --- a/tests/view/writer/insert.js +++ b/tests/view/writer/insert.js @@ -12,6 +12,7 @@ import Position from '../../../src/view/position'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import { stringify, parse } from '../../../src/dev-utils/view'; import AttributeElement from '../../../src/view/attributeelement'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'insert()', () => { @@ -31,7 +32,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'should return collapsed range in insertion position when using empty array', () => { diff --git a/tests/view/writer/mergeattributes.js b/tests/view/writer/mergeattributes.js index 55ba3e748..f4a868e3e 100644 --- a/tests/view/writer/mergeattributes.js +++ b/tests/view/writer/mergeattributes.js @@ -8,6 +8,7 @@ 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'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'mergeAttributes', () => { @@ -25,7 +26,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'should not merge if inside text node', () => { diff --git a/tests/view/writer/mergecontainers.js b/tests/view/writer/mergecontainers.js index 0a2b84f54..bcacd7d29 100644 --- a/tests/view/writer/mergecontainers.js +++ b/tests/view/writer/mergecontainers.js @@ -6,6 +6,7 @@ import Writer from '../../../src/view/writer'; import { stringify, parse } from '../../../src/dev-utils/view'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'mergeContainers()', () => { @@ -24,7 +25,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'should merge two container elements - position between elements', () => { diff --git a/tests/view/writer/move.js b/tests/view/writer/move.js index 0ece379a8..6f7aca776 100644 --- a/tests/view/writer/move.js +++ b/tests/view/writer/move.js @@ -12,6 +12,7 @@ import UIElement from '../../../src/view/uielement'; import Range from '../../../src/view/range'; import Position from '../../../src/view/position'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'move()', () => { @@ -34,7 +35,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'should move single text node', () => { diff --git a/tests/view/writer/remove.js b/tests/view/writer/remove.js index 287102d82..52aa551ac 100644 --- a/tests/view/writer/remove.js +++ b/tests/view/writer/remove.js @@ -12,6 +12,7 @@ import AttributeElement from '../../../src/view/attributeelement'; import EmptyElement from '../../../src/view/emptyelement'; import UIElement from '../../../src/view/uielement'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'remove()', () => { @@ -33,7 +34,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'should throw when range placed in two containers', () => { diff --git a/tests/view/writer/rename.js b/tests/view/writer/rename.js index 28e645904..54df82aa0 100644 --- a/tests/view/writer/rename.js +++ b/tests/view/writer/rename.js @@ -5,13 +5,14 @@ import Writer from '../../../src/view/writer'; import { parse } from '../../../src/dev-utils/view'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'rename()', () => { let root, foo, writer; before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); beforeEach( () => { diff --git a/tests/view/writer/unwrap.js b/tests/view/writer/unwrap.js index 7d258676c..f70507b56 100644 --- a/tests/view/writer/unwrap.js +++ b/tests/view/writer/unwrap.js @@ -14,6 +14,7 @@ import Range from '../../../src/view/range'; import Text from '../../../src/view/text'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import { stringify, parse } from '../../../src/dev-utils/view'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'unwrap()', () => { @@ -32,7 +33,7 @@ describe( 'Writer', () => { } before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); it( 'should do nothing on collapsed ranges', () => { diff --git a/tests/view/writer/wrap.js b/tests/view/writer/wrap.js index 9387c1877..ed8408e94 100644 --- a/tests/view/writer/wrap.js +++ b/tests/view/writer/wrap.js @@ -17,13 +17,14 @@ import Text from '../../../src/view/text'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import { stringify, parse } from '../../../src/dev-utils/view'; import createViewRoot from '../_utils/createroot'; +import Document from '../../../src/view/document'; describe( 'Writer', () => { describe( 'wrap()', () => { let writer; before( () => { - writer = new Writer(); + writer = new Writer( new Document() ); } ); describe( 'non-collapsed range', () => { From cfddbb142dcc61d2e18c8b2e6644fa0c7994388d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Fri, 26 Jan 2018 14:32:32 +0100 Subject: [PATCH 25/89] Added one more test to placeholder. --- tests/view/placeholder.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js index 047bc1d71..c0459fe2d 100644 --- a/tests/view/placeholder.js +++ b/tests/view/placeholder.js @@ -155,6 +155,24 @@ describe( 'placeholder', () => { expect( secondElement.getAttribute( 'data-placeholder' ) ).to.equal( 'second placeholder' ); expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.false; } ); + + it( 'should update placeholder before rendering', () => { + setData( view, '
{another div}
' ); + const element = viewRoot.getChild( 0 ); + + attachPlaceholder( view, element, 'foo bar baz' ); + + view.change( () => { + viewDocument.selection.setRanges( [ ViewRange.createIn( element ) ] ); + + // Here we are before rendering - placeholder is visible in first element; + expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); + expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; + } ); + + // After rendering - placeholder should be invisible since selection is moved there. + expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; + } ); } ); describe( 'detachPlaceholder', () => { From a78fe24462136db26724abf8ef684a6d42a61c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 29 Jan 2018 09:27:30 +0100 Subject: [PATCH 26/89] Added empty view writer test file. --- tests/view/writer/writer.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/view/writer/writer.js diff --git a/tests/view/writer/writer.js b/tests/view/writer/writer.js new file mode 100644 index 000000000..e598434a1 --- /dev/null +++ b/tests/view/writer/writer.js @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import Writer from '../../../src/view/writer'; +import Document from '../../../src/view/document'; +import Text from '../../../src/view/text'; + +describe( 'Writer', () => { + let writer; + + before( () => { + writer = new Writer( new Document() ); + } ); + + describe( 'createText()', () => { + it( 'should create Text instance', () => { + const text = writer.createText( 'foo bar' ); + + expect( text ).to.be.instanceOf( Text ); + expect( text.data ).to.equal( 'foo bar' ); + } ); + } ); +} ); From 6d622c72d66503b178bcf93479009052898fdceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 29 Jan 2018 14:17:24 +0100 Subject: [PATCH 27/89] Moved view selection method access to view writer. --- src/view/selection.js | 10 ++- src/view/writer.js | 50 ++++++++++++ tests/view/selection.js | 166 ++++++++++++++++++++-------------------- 3 files changed, 139 insertions(+), 87 deletions(-) diff --git a/src/view/selection.js b/src/view/selection.js index f2d8079bd..c75f6927d 100644 --- a/src/view/selection.js +++ b/src/view/selection.js @@ -97,7 +97,7 @@ export default class Selection { this._fakeSelectionLabel = ''; if ( selectable ) { - this.setTo( selectable, backwardSelectionOrOffset ); + this._setTo( selectable, backwardSelectionOrOffset ); } } @@ -429,12 +429,13 @@ export default class Selection { * * // Removes all ranges. * selection.setTo( null ); - + * + * @protected * @param {module:engine/view/selection~Selection|module:engine/view/position~Position| * Iterable.|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset] */ - setTo( selectable, backwardSelectionOrOffset ) { + _setTo( selectable, backwardSelectionOrOffset ) { if ( selectable === null ) { this._removeAllRanges(); } else if ( selectable instanceof Selection ) { @@ -489,12 +490,13 @@ export default class Selection { * * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters. * + * @protected * @fires change * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when * first parameter is a {@link module:engine/view/item~Item view item}. */ - setFocus( itemOrPosition, offset ) { + _setFocus( itemOrPosition, offset ) { if ( this.anchor === null ) { /** * Cannot set selection focus if there are no ranges in selection. diff --git a/src/view/writer.js b/src/view/writer.js index 3a9babda0..14c234c5a 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -30,6 +30,56 @@ export default class Writer { this.document = document; } + /** + * Sets {@link module:engine/view/selection~Selection selection's} ranges and direction to the specified location based on the given + * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position}, + * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range}, + * an iterable of {@link module:engine/view/range~Range ranges} or null. + * + * // Sets ranges from the given range. + * const range = new Range( start, end ); + * writer.setSelection( range, isBackwardSelection ); + * + * // Sets ranges from the iterable of ranges. + * const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ]; + * writer.setSelection( range, isBackwardSelection ); + * + * // Sets ranges from the other selection. + * const otherSelection = new Selection(); + * writer.setSelection( otherSelection ); + * + * // Sets collapsed range at the given position. + * const position = new Position( root, path ); + * writer.setSelection( position ); + * + * // Sets collapsed range on the given item. + * const paragraph = writer.createElement( 'paragraph' ); + * writer.setSelection( paragraph, offset ); + * + * // Removes all ranges. + * writer.setSelection( null ); + + * @param {module:engine/view/selection~Selection|module:engine/view/position~Position| + * Iterable.|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable + * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset] + */ + setSelection( selectable, backwardSelectionOrOffset ) { + this.document.selection.setTo( selectable, backwardSelectionOrOffset ); + } + + /** + * Moves {@link module:engine/view/selection~Selection selection's} {@link #focus} to the specified location. + * + * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters. + * + * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition + * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when + * first parameter is a {@link module:engine/view/item~Item view item}. + */ + setSelectionFocus( itemOrPosition, offset ) { + this.model.document.selection.setFocus( itemOrPosition, offset ); + } + /** * Creates a new {@link module:engine/view/text~Text text node}. * diff --git a/tests/view/selection.js b/tests/view/selection.js index 6bfaee053..5611539d9 100644 --- a/tests/view/selection.js +++ b/tests/view/selection.js @@ -132,7 +132,7 @@ describe( 'Selection', () => { } ); it( 'should return start of single range in selection', () => { - selection.setTo( range1 ); + selection._setTo( range1 ); const anchor = selection.anchor; expect( anchor.isEqual( range1.start ) ).to.be.true; @@ -140,7 +140,7 @@ describe( 'Selection', () => { } ); it( 'should return end of single range in selection when added as backward', () => { - selection.setTo( range1, true ); + selection._setTo( range1, true ); const anchor = selection.anchor; expect( anchor.isEqual( range1.end ) ).to.be.true; @@ -148,7 +148,7 @@ describe( 'Selection', () => { } ); it( 'should get anchor from last inserted range', () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); expect( selection.anchor.isEqual( range2.start ) ).to.be.true; } ); @@ -160,14 +160,14 @@ describe( 'Selection', () => { } ); it( 'should return end of single range in selection', () => { - selection.setTo( range1 ); + selection._setTo( range1 ); const focus = selection.focus; expect( focus.isEqual( range1.end ) ).to.be.true; } ); it( 'should return start of single range in selection when added as backward', () => { - selection.setTo( range1, true ); + selection._setTo( range1, true ); const focus = selection.focus; expect( focus.isEqual( range1.start ) ).to.be.true; @@ -175,16 +175,16 @@ describe( 'Selection', () => { } ); it( 'should get focus from last inserted range', () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); expect( selection.focus.isEqual( range2.end ) ).to.be.true; } ); } ); - describe( 'setFocus', () => { + describe( '_setFocus()', () => { it( 'keeps all existing ranges when no modifications needed', () => { - selection.setTo( range1 ); - selection.setFocus( selection.focus ); + selection._setTo( range1 ); + selection._setFocus( selection.focus ); expect( count( selection.getRanges() ) ).to.equal( 1 ); } ); @@ -193,7 +193,7 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 'end' ); expect( () => { - selection.setFocus( endPos ); + selection._setFocus( endPos ); } ).to.throw( CKEditorError, /view-selection-setFocus-no-ranges/ ); } ); @@ -201,9 +201,9 @@ describe( 'Selection', () => { const startPos = Position.createAt( el, 1 ); const endPos = Position.createAt( el, 2 ); - selection.setTo( startPos ); + selection._setTo( startPos ); - selection.setFocus( endPos ); + selection._setFocus( endPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' ); @@ -213,9 +213,9 @@ describe( 'Selection', () => { const startPos = Position.createAt( el, 1 ); const endPos = Position.createAt( el, 0 ); - selection.setTo( startPos ); + selection._setTo( startPos ); - selection.setFocus( endPos ); + selection._setFocus( endPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' ); @@ -227,9 +227,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 3 ); - selection.setTo( new Range( startPos, endPos ) ); + selection._setTo( new Range( startPos, endPos ) ); - selection.setFocus( newEndPos ); + selection._setFocus( newEndPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -240,9 +240,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 0 ); - selection.setTo( new Range( startPos, endPos ) ); + selection._setTo( new Range( startPos, endPos ) ); - selection.setFocus( newEndPos ); + selection._setFocus( newEndPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -254,9 +254,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 3 ); - selection.setTo( new Range( startPos, endPos ), true ); + selection._setTo( new Range( startPos, endPos ), true ); - selection.setFocus( newEndPos ); + selection._setFocus( newEndPos ); expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -268,9 +268,9 @@ describe( 'Selection', () => { const endPos = Position.createAt( el, 2 ); const newEndPos = Position.createAt( el, 0 ); - selection.setTo( new Range( startPos, endPos ), true ); + selection._setTo( new Range( startPos, endPos ), true ); - selection.setFocus( newEndPos ); + selection._setFocus( newEndPos ); expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -286,12 +286,12 @@ describe( 'Selection', () => { const newEndPos = Position.createAt( el, 0 ); - selection.setTo( [ + selection._setTo( [ new Range( startPos1, endPos1 ), new Range( startPos2, endPos2 ) ] ); - selection.setFocus( newEndPos ); + selection._setFocus( newEndPos ); const ranges = Array.from( selection.getRanges() ); @@ -308,9 +308,9 @@ describe( 'Selection', () => { const startPos = Position.createAt( el, 1 ); const endPos = Position.createAt( el, 2 ); - selection.setTo( new Range( startPos, endPos ) ); + selection._setTo( new Range( startPos, endPos ) ); - selection.setFocus( startPos ); + selection._setFocus( startPos ); expect( selection.focus.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.isCollapsed ).to.be.true; @@ -323,8 +323,8 @@ describe( 'Selection', () => { const spy = sinon.stub( Position, 'createAt' ).returns( newEndPos ); - selection.setTo( new Range( startPos, endPos ) ); - selection.setFocus( el, 'end' ); + selection._setTo( new Range( startPos, endPos ) ); + selection._setFocus( el, 'end' ); expect( spy.calledOnce ).to.be.true; expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' ); @@ -336,7 +336,7 @@ describe( 'Selection', () => { describe( 'isCollapsed', () => { it( 'should return true when there is single collapsed range', () => { const range = Range.createFromParentsAndOffsets( el, 5, el, 5 ); - selection.setTo( range ); + selection._setTo( range ); expect( selection.isCollapsed ).to.be.true; } ); @@ -344,14 +344,14 @@ describe( 'Selection', () => { it( 'should return false when there are multiple ranges', () => { const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 ); const range2 = Range.createFromParentsAndOffsets( el, 15, el, 15 ); - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); expect( selection.isCollapsed ).to.be.false; } ); it( 'should return false when there is not collapsed range', () => { const range = Range.createFromParentsAndOffsets( el, 15, el, 16 ); - selection.setTo( range ); + selection._setTo( range ); expect( selection.isCollapsed ).to.be.false; } ); @@ -361,11 +361,11 @@ describe( 'Selection', () => { it( 'should return proper range count', () => { expect( selection.rangeCount ).to.equal( 0 ); - selection.setTo( range1 ); + selection._setTo( range1 ); expect( selection.rangeCount ).to.equal( 1 ); - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); expect( selection.rangeCount ).to.equal( 2 ); } ); @@ -376,17 +376,17 @@ describe( 'Selection', () => { const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 ); const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 ); - selection.setTo( range1, true ); + selection._setTo( range1, true ); expect( selection ).to.have.property( 'isBackward', true ); - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); expect( selection ).to.have.property( 'isBackward', false ); } ); it( 'is false when last range is collapsed', () => { const range = Range.createFromParentsAndOffsets( el, 5, el, 5 ); - selection.setTo( range, true ); + selection._setTo( range, true ); expect( selection.isBackward ).to.be.false; } ); @@ -394,7 +394,7 @@ describe( 'Selection', () => { describe( 'getRanges', () => { it( 'should return iterator with copies of all ranges', () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); const iterable = selection.getRanges(); const ranges = Array.from( iterable ); @@ -409,7 +409,7 @@ describe( 'Selection', () => { describe( 'getFirstRange', () => { it( 'should return copy of range with first position', () => { - selection.setTo( [ range1, range2, range3 ] ); + selection._setTo( [ range1, range2, range3 ] ); const range = selection.getFirstRange(); @@ -424,7 +424,7 @@ describe( 'Selection', () => { describe( 'getLastRange', () => { it( 'should return copy of range with last position', () => { - selection.setTo( [ range1, range2, range3 ] ); + selection._setTo( [ range1, range2, range3 ] ); const range = selection.getLastRange(); @@ -439,7 +439,7 @@ describe( 'Selection', () => { describe( 'getFirstPosition', () => { it( 'should return copy of first position', () => { - selection.setTo( [ range1, range2, range3 ] ); + selection._setTo( [ range1, range2, range3 ] ); const position = selection.getFirstPosition(); @@ -454,7 +454,7 @@ describe( 'Selection', () => { describe( 'getLastPosition', () => { it( 'should return copy of range with last position', () => { - selection.setTo( [ range1, range2, range3 ] ); + selection._setTo( [ range1, range2, range3 ] ); const position = selection.getLastPosition(); @@ -469,16 +469,16 @@ describe( 'Selection', () => { describe( 'isEqual', () => { it( 'should return true if selections equal', () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); const otherSelection = new Selection(); - otherSelection.setTo( [ range1, range2 ] ); + otherSelection._setTo( [ range1, range2 ] ); expect( selection.isEqual( otherSelection ) ).to.be.true; } ); it( 'should return true if backward selections equal', () => { - selection.setTo( range1, true ); + selection._setTo( range1, true ); const otherSelection = new Selection( [ range1 ], true ); @@ -486,7 +486,7 @@ describe( 'Selection', () => { } ); it( 'should return false if ranges count does not equal', () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); const otherSelection = new Selection( [ range1 ] ); @@ -494,7 +494,7 @@ describe( 'Selection', () => { } ); it( 'should return false if ranges (other than the last added one) do not equal', () => { - selection.setTo( [ range1, range3 ] ); + selection._setTo( [ range1, range3 ] ); const otherSelection = new Selection( [ range2, range3 ] ); @@ -502,7 +502,7 @@ describe( 'Selection', () => { } ); it( 'should return false if directions do not equal', () => { - selection.setTo( range1 ); + selection._setTo( range1 ); const otherSelection = new Selection( [ range1 ], true ); @@ -520,7 +520,7 @@ describe( 'Selection', () => { const otherSelection = new Selection( [ range1 ] ); otherSelection.setFake( true ); selection.setFake( true ); - selection.setTo( range1 ); + selection._setTo( range1 ); expect( selection.isEqual( otherSelection ) ).to.be.true; } ); @@ -529,7 +529,7 @@ describe( 'Selection', () => { const otherSelection = new Selection( [ range1 ] ); otherSelection.setFake( true, { label: 'foo bar baz' } ); selection.setFake( true ); - selection.setTo( range1 ); + selection._setTo( range1 ); expect( selection.isEqual( otherSelection ) ).to.be.false; } ); @@ -543,7 +543,7 @@ describe( 'Selection', () => { describe( 'isSimilar', () => { it( 'should return true if selections equal', () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); const otherSelection = new Selection( [ range1, range2 ] ); @@ -551,7 +551,7 @@ describe( 'Selection', () => { } ); it( 'should return false if ranges count does not equal', () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); const otherSelection = new Selection( [ range1 ] ); @@ -559,7 +559,7 @@ describe( 'Selection', () => { } ); it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => { - selection.setTo( [ range1, range3 ] ); + selection._setTo( [ range1, range3 ] ); const otherSelection = new Selection( [ range2, range3 ] ); @@ -567,7 +567,7 @@ describe( 'Selection', () => { } ); it( 'should return false if directions are not equal', () => { - selection.setTo( range1 ); + selection._setTo( range1 ); const otherSelection = new Selection( [ range1 ], true ); @@ -597,7 +597,7 @@ describe( 'Selection', () => { const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 ); const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 ); - selection.setTo( [ rangeA1, rangeA2 ] ); + selection._setTo( [ rangeA1, rangeA2 ] ); const otherSelection = new Selection( [ rangeB2, rangeB1 ] ); @@ -617,7 +617,7 @@ describe( 'Selection', () => { } ); it( 'should add ranges and fire change event', done => { - selection.setTo( range1 ); + selection._setTo( range1 ); selection.once( 'change', () => { expect( selection.rangeCount ).to.equal( 2 ); @@ -641,14 +641,14 @@ describe( 'Selection', () => { } ); } ); - describe( 'setTo()', () => { + describe( '_setTo()', () => { describe( 'simple scenarios', () => { it( 'should set selection ranges from the given selection', () => { - selection.setTo( range1 ); + selection._setTo( range1 ); const otherSelection = new Selection( [ range2, range3 ], true ); - selection.setTo( otherSelection ); + selection._setTo( otherSelection ); expect( selection.rangeCount ).to.equal( 2 ); expect( selection._ranges[ 0 ].isEqual( range2 ) ).to.be.true; @@ -662,7 +662,7 @@ describe( 'Selection', () => { it( 'should set selection on the given Range using _setRanges method', () => { const spy = sinon.spy( selection, '_setRanges' ); - selection.setTo( range1 ); + selection._setTo( range1 ); expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] ); expect( selection.isBackward ).to.be.false; @@ -673,7 +673,7 @@ describe( 'Selection', () => { it( 'should set selection on the given iterable of Ranges using _setRanges method', () => { const spy = sinon.spy( selection, '_setRanges' ); - selection.setTo( new Set( [ range1, range2 ] ) ); + selection._setTo( new Set( [ range1, range2 ] ) ); expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] ); expect( selection.isBackward ).to.be.false; @@ -684,7 +684,7 @@ describe( 'Selection', () => { it( 'should set collapsed selection on the given Position using _setRanges method', () => { const spy = sinon.spy( selection, '_setRanges' ); - selection.setTo( range1.start ); + selection._setTo( range1.start ); expect( Array.from( selection.getRanges() ).length ).to.equal( 1 ); expect( Array.from( selection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start ); @@ -703,14 +703,14 @@ describe( 'Selection', () => { const otherSelection = new Selection( [ range1 ] ); - selection.setTo( otherSelection ); + selection._setTo( otherSelection ); } ); it( 'should set fake state and label', () => { const otherSelection = new Selection(); const label = 'foo bar baz'; otherSelection.setFake( true, { label } ); - selection.setTo( otherSelection ); + selection._setTo( otherSelection ); expect( selection.isFake ).to.be.true; expect( selection.fakeSelectionLabel ).to.equal( label ); @@ -720,7 +720,7 @@ describe( 'Selection', () => { const otherSelection = new Selection(); expect( () => { - otherSelection.setTo( {} ); + otherSelection._setTo( {} ); } ).to.throw( /view-selection-setTo-not-selectable/ ); } ); @@ -728,20 +728,20 @@ describe( 'Selection', () => { const otherSelection = new Selection(); expect( () => { - otherSelection.setTo(); + otherSelection._setTo(); } ).to.throw( /view-selection-setTo-not-selectable/ ); } ); } ); describe( 'setting collapsed selection', () => { beforeEach( () => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); } ); it( 'should collapse selection at position', () => { const position = new Position( el, 4 ); - selection.setTo( position ); + selection._setTo( position ); const range = selection.getFirstRange(); expect( range.start.parent ).to.equal( el ); @@ -753,14 +753,14 @@ describe( 'Selection', () => { const foo = new Text( 'foo' ); const p = new Element( 'p', null, foo ); - selection.setTo( foo ); + selection._setTo( foo ); let range = selection.getFirstRange(); expect( range.start.parent ).to.equal( foo ); expect( range.start.offset ).to.equal( 0 ); expect( range.start.isEqual( range.end ) ).to.be.true; - selection.setTo( p, 1 ); + selection._setTo( p, 1 ); range = selection.getFirstRange(); expect( range.start.parent ).to.equal( p ); @@ -772,21 +772,21 @@ describe( 'Selection', () => { const foo = new Text( 'foo' ); const p = new Element( 'p', null, foo ); - selection.setTo( foo, 'end' ); + selection._setTo( foo, 'end' ); let range = selection.getFirstRange(); expect( range.start.parent ).to.equal( foo ); expect( range.start.offset ).to.equal( 3 ); expect( range.start.isEqual( range.end ) ).to.be.true; - selection.setTo( foo, 'before' ); + selection._setTo( foo, 'before' ); range = selection.getFirstRange(); expect( range.start.parent ).to.equal( p ); expect( range.start.offset ).to.equal( 0 ); expect( range.start.isEqual( range.end ) ).to.be.true; - selection.setTo( foo, 'after' ); + selection._setTo( foo, 'after' ); range = selection.getFirstRange(); expect( range.start.parent ).to.equal( p ); @@ -797,7 +797,7 @@ describe( 'Selection', () => { describe( 'setting collapsed selection at start', () => { it( 'should collapse to start position and fire change event', done => { - selection.setTo( [ range1, range2, range3 ] ); + selection._setTo( [ range1, range2, range3 ] ); selection.once( 'change', () => { expect( selection.rangeCount ).to.equal( 1 ); expect( selection.isCollapsed ).to.be.true; @@ -805,13 +805,13 @@ describe( 'Selection', () => { done(); } ); - selection.setTo( selection.getFirstPosition() ); + selection._setTo( selection.getFirstPosition() ); } ); it( 'should do nothing if no ranges present', () => { const fireSpy = sinon.spy( selection, 'fire' ); - selection.setTo( selection.getFirstPosition() ); + selection._setTo( selection.getFirstPosition() ); fireSpy.restore(); expect( fireSpy.notCalled ).to.be.true; @@ -820,7 +820,7 @@ describe( 'Selection', () => { describe( 'setting collapsed selection to end', () => { it( 'should collapse to end position and fire change event', done => { - selection.setTo( [ range1, range2, range3 ] ); + selection._setTo( [ range1, range2, range3 ] ); selection.once( 'change', () => { expect( selection.rangeCount ).to.equal( 1 ); expect( selection.isCollapsed ).to.be.true; @@ -828,13 +828,13 @@ describe( 'Selection', () => { done(); } ); - selection.setTo( selection.getLastPosition() ); + selection._setTo( selection.getLastPosition() ); } ); it( 'should do nothing if no ranges present', () => { const fireSpy = sinon.spy( selection, 'fire' ); - selection.setTo( selection.getLastPosition() ); + selection._setTo( selection.getLastPosition() ); fireSpy.restore(); expect( fireSpy.notCalled ).to.be.true; @@ -843,19 +843,19 @@ describe( 'Selection', () => { describe( 'removing all ranges', () => { it( 'should remove all ranges and fire change event', done => { - selection.setTo( [ range1, range2 ] ); + selection._setTo( [ range1, range2 ] ); selection.once( 'change', () => { expect( selection.rangeCount ).to.equal( 0 ); done(); } ); - selection.setTo( null ); + selection._setTo( null ); } ); it( 'should do nothing when no ranges are present', () => { const fireSpy = sinon.spy( selection, 'fire' ); - selection.setTo( null ); + selection._setTo( null ); fireSpy.restore(); expect( fireSpy.notCalled ).to.be.true; @@ -869,7 +869,7 @@ describe( 'Selection', () => { } ); it( 'should return null if selection is placed in container that is not EditableElement', () => { - selection.setTo( range1 ); + selection._setTo( range1 ); expect( selection.editableElement ).to.be.null; } ); @@ -881,7 +881,7 @@ describe( 'Selection', () => { const element = new Element( 'p' ); root.appendChildren( element ); - selection.setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) ); + selection._setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) ); expect( selection.editableElement ).to.equal( root ); } ); From b8c2bde362a3e3e371abd3a1b93beabc549e7214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 29 Jan 2018 16:14:11 +0100 Subject: [PATCH 28/89] Fixed tests after view selection methods visibility change. --- .../model-selection-to-view-converters.js | 6 +- src/dev-utils/view.js | 2 +- src/view/observer/fakeselectionobserver.js | 4 +- src/view/observer/mutationobserver.js | 4 +- src/view/writer.js | 6 +- .../view-selection-to-model-converters.js | 2 +- tests/dev-utils/view.js | 2 +- tests/view/domconverter/binding.js | 2 +- tests/view/editableelement.js | 12 +- tests/view/observer/focusobserver.js | 4 +- tests/view/observer/mutationobserver.js | 6 +- tests/view/observer/selectionobserver.js | 12 +- tests/view/placeholder.js | 8 +- tests/view/renderer.js | 118 +++++++++--------- tests/view/view/jumpoverinlinefiller.js | 2 +- tests/view/view/jumpoveruielement.js | 28 ++--- tests/view/view/view.js | 6 +- tests/view/writer/wrap.js | 2 +- 18 files changed, 113 insertions(+), 113 deletions(-) diff --git a/src/conversion/model-selection-to-view-converters.js b/src/conversion/model-selection-to-view-converters.js index 3d29e6bc3..bae740b98 100644 --- a/src/conversion/model-selection-to-view-converters.js +++ b/src/conversion/model-selection-to-view-converters.js @@ -39,7 +39,7 @@ export function convertRangeSelection() { viewRanges.push( viewRange ); } - conversionApi.viewSelection.setTo( viewRanges, selection.isBackward ); + conversionApi.viewSelection._setTo( viewRanges, selection.isBackward ); }; } @@ -81,7 +81,7 @@ export function convertCollapsedSelection() { const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); const brokenPosition = conversionApi.writer.breakAttributes( viewPosition ); - conversionApi.viewSelection.setTo( brokenPosition ); + conversionApi.viewSelection._setTo( brokenPosition ); }; } @@ -120,7 +120,7 @@ export function clearAttributes() { } } } - conversionApi.viewSelection.setTo( null ); + conversionApi.viewSelection._setTo( null ); }; } diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js index 12dfaab79..ab5f78ba4 100644 --- a/src/dev-utils/view.js +++ b/src/dev-utils/view.js @@ -92,7 +92,7 @@ export function setData( view, data, options = {} ) { const result = setData._parse( data, { rootElement: root } ); if ( result.view && result.selection ) { - document.selection.setTo( result.selection ); + document.selection._setTo( result.selection ); } } diff --git a/src/view/observer/fakeselectionobserver.js b/src/view/observer/fakeselectionobserver.js index 4bc7b8c49..2663bdac2 100644 --- a/src/view/observer/fakeselectionobserver.js +++ b/src/view/observer/fakeselectionobserver.js @@ -87,12 +87,12 @@ export default class FakeSelectionObserver extends Observer { // Left or up arrow pressed - move selection to start. if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) { - newSelection.setTo( newSelection.getFirstPosition() ); + newSelection._setTo( newSelection.getFirstPosition() ); } // Right or down arrow pressed - move selection to end. if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) { - newSelection.setTo( newSelection.getLastPosition() ); + newSelection._setTo( newSelection.getLastPosition() ); } const data = { diff --git a/src/view/observer/mutationobserver.js b/src/view/observer/mutationobserver.js index 73498c792..f733a228f 100644 --- a/src/view/observer/mutationobserver.js +++ b/src/view/observer/mutationobserver.js @@ -240,8 +240,8 @@ export default class MutationObserver extends Observer { // Anchor and focus has to be properly mapped to view. if ( viewSelectionAnchor && viewSelectionFocus ) { viewSelection = new ViewSelection(); - viewSelection.setTo( viewSelectionAnchor ); - viewSelection.setFocus( viewSelectionFocus ); + viewSelection._setTo( viewSelectionAnchor ); + viewSelection._setFocus( viewSelectionFocus ); } } diff --git a/src/view/writer.js b/src/view/writer.js index 14c234c5a..f903026bc 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -64,7 +64,7 @@ export default class Writer { * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset] */ setSelection( selectable, backwardSelectionOrOffset ) { - this.document.selection.setTo( selectable, backwardSelectionOrOffset ); + this.document.selection._setTo( selectable, backwardSelectionOrOffset ); } /** @@ -77,7 +77,7 @@ export default class Writer { * first parameter is a {@link module:engine/view/item~Item view item}. */ setSelectionFocus( itemOrPosition, offset ) { - this.model.document.selection.setFocus( itemOrPosition, offset ); + this.model.document.selection._setFocus( itemOrPosition, offset ); } /** @@ -639,7 +639,7 @@ export default class Writer { // If wrapping position is equal to view selection, move view selection inside wrapping attribute element. if ( viewSelection && viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) { - viewSelection.setTo( position ); + viewSelection._setTo( position ); } return new Range( position ); diff --git a/tests/conversion/view-selection-to-model-converters.js b/tests/conversion/view-selection-to-model-converters.js index ad25d3960..56c220ec4 100644 --- a/tests/conversion/view-selection-to-model-converters.js +++ b/tests/conversion/view-selection-to-model-converters.js @@ -46,7 +46,7 @@ describe( 'convertSelectionChange', () => { it( 'should convert collapsed selection', () => { const viewSelection = new ViewSelection(); - viewSelection.setTo( ViewRange.createFromParentsAndOffsets( + viewSelection._setTo( ViewRange.createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) ); convertSelection( null, { newSelection: viewSelection } ); diff --git a/tests/dev-utils/view.js b/tests/dev-utils/view.js index 53900eb3e..3e516dc60 100644 --- a/tests/dev-utils/view.js +++ b/tests/dev-utils/view.js @@ -63,7 +63,7 @@ describe( 'view test utils', () => { const root = createAttachedRoot( viewDocument, element ); root.appendChildren( new Element( 'p' ) ); - viewDocument.selection.setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) ); + viewDocument.selection._setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) ); expect( getData( view, options ) ).to.equal( '[

]' ); sinon.assert.calledOnce( stringifySpy ); diff --git a/tests/view/domconverter/binding.js b/tests/view/domconverter/binding.js index 6429d31c1..c853ac3c8 100644 --- a/tests/view/domconverter/binding.js +++ b/tests/view/domconverter/binding.js @@ -283,7 +283,7 @@ describe( 'DomConverter', () => { it( 'should keep a copy of selection', () => { const selectionCopy = new ViewSelection( selection ); - selection.setTo( ViewRange.createIn( new ViewElement() ), true ); + selection._setTo( ViewRange.createIn( new ViewElement() ), true ); const bindSelection = converter.fakeSelectionToView( domEl ); expect( bindSelection ).to.not.equal( selection ); diff --git a/tests/view/editableelement.js b/tests/view/editableelement.js index 7b77fe42b..bb6013446 100644 --- a/tests/view/editableelement.js +++ b/tests/view/editableelement.js @@ -78,13 +78,13 @@ describe( 'EditableElement', () => { it( 'should change isFocused on document render event', () => { const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ); const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 ); - docMock.selection.setTo( rangeMain ); + docMock.selection._setTo( rangeMain ); docMock.isFocused = true; expect( viewMain.isFocused ).to.be.true; expect( viewHeader.isFocused ).to.be.false; - docMock.selection.setTo( [ rangeHeader ] ); + docMock.selection._setTo( [ rangeHeader ] ); docMock.fire( 'render' ); expect( viewMain.isFocused ).to.be.false; @@ -96,13 +96,13 @@ describe( 'EditableElement', () => { const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 ); docMock.render = sinon.spy(); - docMock.selection.setTo( rangeMain ); + docMock.selection._setTo( rangeMain ); docMock.isFocused = true; expect( viewMain.isFocused ).to.be.true; expect( viewHeader.isFocused ).to.be.false; - docMock.selection.setTo( [ rangeHeader ] ); + docMock.selection._setTo( [ rangeHeader ] ); viewHeader.on( 'change:isFocused', ( evt, propertyName, value ) => { expect( value ).to.be.true; @@ -116,7 +116,7 @@ describe( 'EditableElement', () => { it( 'should change isFocused when document.isFocus changes', () => { const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ); const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 ); - docMock.selection.setTo( rangeMain ); + docMock.selection._setTo( rangeMain ); docMock.isFocused = true; expect( viewMain.isFocused ).to.be.true; @@ -127,7 +127,7 @@ describe( 'EditableElement', () => { expect( viewMain.isFocused ).to.be.false; expect( viewHeader.isFocused ).to.be.false; - docMock.selection.setTo( [ rangeHeader ] ); + docMock.selection._setTo( [ rangeHeader ] ); expect( viewMain.isFocused ).to.be.false; expect( viewHeader.isFocused ).to.be.false; diff --git a/tests/view/observer/focusobserver.js b/tests/view/observer/focusobserver.js index 7e2cdbe75..d28018646 100644 --- a/tests/view/observer/focusobserver.js +++ b/tests/view/observer/focusobserver.js @@ -95,7 +95,7 @@ describe( 'FocusObserver', () => { } ); it( 'should set isFocused to false on blur when selection in same editable', () => { - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) ); observer.onDomEvent( { type: 'focus', target: domMain } ); @@ -107,7 +107,7 @@ describe( 'FocusObserver', () => { } ); it( 'should not set isFocused to false on blur when it is fired on other editable', () => { - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) ); observer.onDomEvent( { type: 'focus', target: domMain } ); diff --git a/tests/view/observer/mutationobserver.js b/tests/view/observer/mutationobserver.js index 4bb84db9c..5dd0f9949 100644 --- a/tests/view/observer/mutationobserver.js +++ b/tests/view/observer/mutationobserver.js @@ -26,7 +26,7 @@ describe( 'MutationObserver', () => { createViewRoot( viewDocument ); view.attachDomRoot( domEditor ); - viewDocument.selection.setTo( null ); + viewDocument.selection._setTo( null ); document.getSelection().removeAllRanges(); mutationObserver = view.getObserver( MutationObserver ); @@ -228,7 +228,7 @@ describe( 'MutationObserver', () => { const { view: viewContainer, selection } = parse( 'foo[]bar' ); viewRoot.appendChildren( viewContainer ); - viewDocument.selection.setTo( selection ); + viewDocument.selection._setTo( selection ); view.render(); @@ -246,7 +246,7 @@ describe( 'MutationObserver', () => { const { view: viewContainer, selection } = parse( 'foo[]bar' ); viewRoot.appendChildren( viewContainer ); - viewDocument.selection.setTo( selection ); + viewDocument.selection._setTo( selection ); view.render(); diff --git a/tests/view/observer/selectionobserver.js b/tests/view/observer/selectionobserver.js index 24210db47..69b2dd00b 100644 --- a/tests/view/observer/selectionobserver.js +++ b/tests/view/observer/selectionobserver.js @@ -42,7 +42,7 @@ describe( 'SelectionObserver', () => { view.render(); - viewDocument.selection.setTo( null ); + viewDocument.selection._setTo( null ); domDocument.getSelection().removeAllRanges(); viewDocument.isFocused = true; @@ -104,7 +104,7 @@ describe( 'SelectionObserver', () => { setTimeout( done, 70 ); const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) ); view.render(); } ); @@ -163,7 +163,7 @@ describe( 'SelectionObserver', () => { let counter = 70; const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) ); return new Promise( ( resolve, reject ) => { testUtils.sinon.stub( log, 'warn' ).callsFake( msg => { @@ -187,7 +187,7 @@ describe( 'SelectionObserver', () => { it( 'should not be treated as an infinite loop if selection is changed only few times', done => { const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) ); const spy = testUtils.sinon.spy( log, 'warn' ); viewDocument.on( 'selectionChangeDone', () => { @@ -320,8 +320,8 @@ describe( 'SelectionObserver', () => { const viewAnchor = view.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset ); const viewFocus = view.domConverter.domPositionToView( sel.focusNode, sel.focusOffset ); - viewSel.setTo( viewAnchor ); - viewSel.setFocus( viewFocus ); + viewSel._setTo( viewAnchor ); + viewSel._setFocus( viewFocus ); view.render(); } ); diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js index 92b006bb7..adbecdf7e 100644 --- a/tests/view/placeholder.js +++ b/tests/view/placeholder.js @@ -93,7 +93,7 @@ describe( 'placeholder', () => { expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; view.change( () => { - viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] ); + viewDocument.selection._setTo( [ ViewRange.createIn( element ) ] ); } ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; @@ -142,11 +142,11 @@ describe( 'placeholder', () => { // Move selection to the elements with placeholders. view.change( () => { - viewDocument.selection.setTo( [ ViewRange.createIn( element ) ] ); + viewDocument.selection._setTo( [ ViewRange.createIn( element ) ] ); } ); secondView.change( () => { - secondDocument.selection.setTo( [ ViewRange.createIn( secondElement ) ] ); + secondDocument.selection._setTo( [ ViewRange.createIn( secondElement ) ] ); } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' ); @@ -163,7 +163,7 @@ describe( 'placeholder', () => { attachPlaceholder( view, element, 'foo bar baz' ); view.change( () => { - viewDocument.selection.setTo( ViewRange.createIn( element ) ); + viewDocument.selection._setTo( ViewRange.createIn( element ) ); // Here we are before rendering - placeholder is visible in first element; expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); diff --git a/tests/view/renderer.js b/tests/view/renderer.js index b48e2bf5c..99849ca9c 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -121,7 +121,7 @@ describe( 'Renderer', () => { renderer.markedAttributes.clear(); renderer.markedChildren.clear(); - selection.setTo( null ); + selection._setTo( null ); selection.setFake( false ); selectionEditable = viewRoot; @@ -404,7 +404,7 @@ describe( 'Renderer', () => { const viewRoot = new ViewElement( 'p' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -418,7 +418,7 @@ describe( 'Renderer', () => { 'foo[]bar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -440,7 +440,7 @@ describe( 'Renderer', () => { 'foo[]bar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -465,7 +465,7 @@ describe( 'Renderer', () => { 'foo[]bar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -494,7 +494,7 @@ describe( 'Renderer', () => { renderAndExpectNoChanges( renderer, domRoot ); // Step 3:

foo{}

- selection.setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ), 3, viewP.getChild( 0 ), 3 ) ); renderer.render(); @@ -524,7 +524,7 @@ describe( 'Renderer', () => { '[]foo' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -547,7 +547,7 @@ describe( 'Renderer', () => { renderAndExpectNoChanges( renderer, domRoot ); // Step 3:

{}foo

- selection.setTo( ViewRange.createFromParentsAndOffsets( + selection._setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ).getChild( 0 ), 0, viewP.getChild( 0 ).getChild( 0 ), 0 ) ); renderer.render(); @@ -575,7 +575,7 @@ describe( 'Renderer', () => { 'foo[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -598,7 +598,7 @@ describe( 'Renderer', () => { renderAndExpectNoChanges( renderer, domRoot ); // Step 3:

foo{}

- selection.setTo( ViewRange.createFromParentsAndOffsets( + selection._setTo( ViewRange.createFromParentsAndOffsets( viewP.getChild( 0 ).getChild( 0 ), 3, viewP.getChild( 0 ).getChild( 0 ), 3 ) ); renderer.render(); @@ -625,7 +625,7 @@ describe( 'Renderer', () => { 'foo[]bar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -653,7 +653,7 @@ describe( 'Renderer', () => { 'foo[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); @@ -671,7 +671,7 @@ describe( 'Renderer', () => { // Step 2:

foo"FILLER{}"

const viewI = viewP.getChild( 2 ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) ); renderer.render(); @@ -689,7 +689,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( 'foo[]' ); const viewB = viewP.getChild( 1 ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -704,13 +704,13 @@ describe( 'Renderer', () => { // Step 2: Add text node. const viewText = new ViewText( 'x' ); viewB.appendChildren( viewText ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); renderer.markToSync( 'children', viewB ); renderer.render(); // Step 3: Remove selection from the view. - selection.setTo( null ); + selection._setTo( null ); renderer.render(); @@ -728,7 +728,7 @@ describe( 'Renderer', () => { // Step 1:

barfoo"FILLER{}"

const { view: viewP, selection: newSelection } = parse( 'barfoo[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -740,7 +740,7 @@ describe( 'Renderer', () => { // Step 2: Remove the and update the selection (

bar[]

). viewP.removeChildren( 1 ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 1, viewP, 1 ) ); renderer.markToSync( 'children', viewP ); renderer.render(); @@ -757,7 +757,7 @@ describe( 'Renderer', () => { 'foo[]bar' ); viewRoot.appendChildren( viewFragment ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -777,7 +777,7 @@ describe( 'Renderer', () => { viewP2.appendChildren( removedChildren ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) ); renderer.markToSync( 'children', viewP ); renderer.markToSync( 'children', viewP2 ); @@ -799,7 +799,7 @@ describe( 'Renderer', () => { // Step 1:

bar"FILLER{}"

const { view: viewP, selection: newSelection } = parse( 'bar[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -814,7 +814,7 @@ describe( 'Renderer', () => { const viewI = parse( '' ); viewP.appendChildren( viewI ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewI, 0, viewI, 0 ) ); renderer.markToSync( 'children', viewP ); renderer.render(); @@ -831,7 +831,7 @@ describe( 'Renderer', () => { // Step 1:

barabc"FILLER"{}

const { view: viewP, selection: newSelection } = parse( 'barabc[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -844,7 +844,7 @@ describe( 'Renderer', () => { const viewAbc = parse( 'abc' ); viewP.appendChildren( viewAbc ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 3, viewP, 3 ) ); renderer.markToSync( 'children', viewP ); renderer.render(); @@ -860,7 +860,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( '[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -887,7 +887,7 @@ describe( 'Renderer', () => { const viewText = new ViewText( 'x' ); viewP.appendChildren( viewText ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); renderer.markToSync( 'children', viewP ); renderAndExpectNoChanges( renderer, domRoot ); @@ -899,7 +899,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( '[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -917,7 +917,7 @@ describe( 'Renderer', () => { // Add text node only in View

x{}

const viewText = new ViewText( 'x' ); viewP.appendChildren( viewText ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); renderer.markToSync( 'children', viewP ); renderer.render(); @@ -937,7 +937,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( 'x{}' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -964,7 +964,7 @@ describe( 'Renderer', () => { viewP.removeChildren( 0 ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewP, 0, viewP, 0 ) ); renderer.markToSync( 'children', viewP ); renderAndExpectNoChanges( renderer, domRoot ); @@ -979,7 +979,7 @@ describe( 'Renderer', () => { '[]foo' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1015,7 +1015,7 @@ describe( 'Renderer', () => { const viewText = new ViewText( 'x' ); viewB.appendChildren( viewText ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); renderer.markToSync( 'children', viewP ); renderAndExpectNoChanges( renderer, domRoot ); @@ -1030,7 +1030,7 @@ describe( 'Renderer', () => { '[]foo' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1058,7 +1058,7 @@ describe( 'Renderer', () => { const viewText = new ViewText( 'x' ); viewB.appendChildren( viewText ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); renderer.markToSync( 'children', viewB ); renderer.render(); @@ -1093,7 +1093,7 @@ describe( 'Renderer', () => { '[]foo' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1121,7 +1121,7 @@ describe( 'Renderer', () => { const viewText = new ViewText( 'x' ); viewB.appendChildren( viewText ); - selection.setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); + selection._setTo( ViewRange.createFromParentsAndOffsets( viewText, 1, viewText, 1 ) ); renderer.markToSync( 'text', viewText ); renderer.render(); @@ -1144,7 +1144,7 @@ describe( 'Renderer', () => { 'fo{ob}ar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1182,7 +1182,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( 'fo{o}' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.render(); @@ -1224,7 +1224,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( 'fo{o}' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.render(); @@ -1253,7 +1253,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( 'foo[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1268,7 +1268,7 @@ describe( 'Renderer', () => { 'foo[]bar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1281,7 +1281,7 @@ describe( 'Renderer', () => { // Remove filler. domB.childNodes[ 0 ].data = ''; - selection.setTo( null ); + selection._setTo( null ); renderer.markToSync( 'children', viewB ); expect( () => { @@ -1301,7 +1301,7 @@ describe( 'Renderer', () => { const { view: view, selection: newSelection } = parse( inputView ); viewRoot.appendChildren( view ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1362,7 +1362,7 @@ describe( 'Renderer', () => { '[foo bar]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); } ); @@ -1552,7 +1552,7 @@ describe( 'Renderer', () => { 'foo{}bar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1572,7 +1572,7 @@ describe( 'Renderer', () => { selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' ); // foo{}bar - selection.setTo( [ + selection._setTo( [ new ViewRange( new ViewPosition( viewB.getChild( 0 ), 0 ), new ViewPosition( viewB.getChild( 0 ), 0 ) ) ] ); @@ -1592,7 +1592,7 @@ describe( 'Renderer', () => { 'foo[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1611,7 +1611,7 @@ describe( 'Renderer', () => { selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' ); // foo{} - selection.setTo( [ + selection._setTo( [ new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewP.getChild( 0 ), 3 ) ) ] ); @@ -1631,7 +1631,7 @@ describe( 'Renderer', () => { 'fo{o}bar' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1651,7 +1651,7 @@ describe( 'Renderer', () => { selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' ); // fo{ob}ar - selection.setTo( [ + selection._setTo( [ new ViewRange( new ViewPosition( viewP.getChild( 0 ), 2 ), new ViewPosition( viewB.getChild( 0 ), 1 ) ) ] ); @@ -1670,7 +1670,7 @@ describe( 'Renderer', () => { const { view: viewP, selection: newSelection } = parse( 'foo[]' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1712,7 +1712,7 @@ describe( 'Renderer', () => { 'foo{ba}r' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1732,7 +1732,7 @@ describe( 'Renderer', () => { selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' ); // foo{ba}r - selection.setTo( [ + selection._setTo( [ new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewB.getChild( 0 ), 2 ) ) ] ); @@ -1750,7 +1750,7 @@ describe( 'Renderer', () => { 'foob{ar}baz' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1770,7 +1770,7 @@ describe( 'Renderer', () => { selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' ); // foob{ar}baz - selection.setTo( [ + selection._setTo( [ new ViewRange( new ViewPosition( viewB.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) ) ] ); @@ -1788,7 +1788,7 @@ describe( 'Renderer', () => { 'foo{ba}r' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1808,7 +1808,7 @@ describe( 'Renderer', () => { selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' ); // foo{ba}r - selection.setTo( [ + selection._setTo( [ new ViewRange( new ViewPosition( viewP.getChild( 0 ), 3 ), new ViewPosition( viewI.getChild( 0 ), 2 ) ) ] ); @@ -1826,7 +1826,7 @@ describe( 'Renderer', () => { 'f{oobar}baz' ); viewRoot.appendChildren( viewP ); - selection.setTo( newSelection ); + selection._setTo( newSelection ); renderer.markToSync( 'children', viewRoot ); renderer.render(); @@ -1845,7 +1845,7 @@ describe( 'Renderer', () => { selectionExtendSpy = sinon.spy( window.Selection.prototype, 'extend' ); // f{oobar}baz - selection.setTo( [ + selection._setTo( [ new ViewRange( new ViewPosition( viewP.getChild( 0 ), 1 ), new ViewPosition( viewP.getChild( 2 ), 0 ) ) ] ); diff --git a/tests/view/view/jumpoverinlinefiller.js b/tests/view/view/jumpoverinlinefiller.js index 78178c22a..47837df1c 100644 --- a/tests/view/view/jumpoverinlinefiller.js +++ b/tests/view/view/jumpoverinlinefiller.js @@ -113,7 +113,7 @@ describe( 'View', () => { const viewB = viewDocument.selection.getFirstPosition().parent; const viewTextX = parse( 'x' ); viewB.appendChildren( viewTextX ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) ); const domB = view.getDomRoot( 'main' ).querySelector( 'b' ); const domSelection = document.getSelection(); diff --git a/tests/view/view/jumpoveruielement.js b/tests/view/view/jumpoveruielement.js index df47ec53a..fc67cd9ef 100644 --- a/tests/view/view/jumpoveruielement.js +++ b/tests/view/view/jumpoveruielement.js @@ -17,7 +17,7 @@ import createViewRoot from '../_utils/createroot'; import { setData as setViewData } from '../../../src/dev-utils/view'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -describe( 'Document', () => { +describe( 'View', () => { let view, viewDocument, domRoot, domSelection, viewRoot, foo, bar, ui, ui2; function createUIElement( name, contents ) { @@ -90,7 +90,7 @@ describe( 'Document', () => { // fooxxx{}bar const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] ); + viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] ); renderAndFireKeydownEvent( { keyCode: keyCodes.arrowleft } ); @@ -105,7 +105,7 @@ describe( 'Document', () => { // foo[]xxxbar const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] ); + viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] ); renderAndFireKeydownEvent(); @@ -122,7 +122,7 @@ describe( 'Document', () => { // foo{}xxxbar const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); renderAndFireKeydownEvent(); @@ -139,7 +139,7 @@ describe( 'Document', () => { // foo{}xxxyyybar' const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); renderAndFireKeydownEvent(); @@ -158,7 +158,7 @@ describe( 'Document', () => { const div = new ViewContainerElement( 'div' ); viewRoot.appendChildren( p ); viewRoot.appendChildren( div ); - viewDocument.selection.setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); renderAndFireKeydownEvent(); @@ -176,7 +176,7 @@ describe( 'Document', () => { const b = new ViewAttribtueElement( 'b', null, foo ); const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); renderAndFireKeydownEvent(); @@ -194,7 +194,7 @@ describe( 'Document', () => { const b = new ViewAttribtueElement( 'b', null, foo ); const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) ); renderAndFireKeydownEvent(); @@ -222,7 +222,7 @@ describe( 'Document', () => { const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); renderAndFireKeydownEvent(); @@ -249,7 +249,7 @@ describe( 'Document', () => { const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); renderAndFireKeydownEvent(); @@ -277,7 +277,7 @@ describe( 'Document', () => { const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); renderAndFireKeydownEvent( { shiftKey: true } ); @@ -353,7 +353,7 @@ describe( 'Document', () => { const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); renderAndFireKeydownEvent( { shiftKey: true } ); @@ -378,7 +378,7 @@ describe( 'Document', () => { const i = new ViewAttribtueElement( 'i', null, b ); const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); renderAndFireKeydownEvent( { shiftKey: true } ); @@ -404,7 +404,7 @@ describe( 'Document', () => { const b2 = new ViewAttribtueElement( 'b' ); const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); renderAndFireKeydownEvent( { shiftKey: true } ); diff --git a/tests/view/view/view.js b/tests/view/view/view.js index c10a03423..5d9579d68 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -255,7 +255,7 @@ describe( 'view', () => { left: '-1000px' } ); - viewDocument.selection.setTo( range ); + viewDocument.selection._setTo( range ); view.scrollToTheSelection(); sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number ); @@ -301,7 +301,7 @@ describe( 'view', () => { document.body.appendChild( domEditable ); viewEditable = createViewRoot( viewDocument, 'div', 'main' ); view.attachDomRoot( domEditable ); - viewDocument.selection.setTo( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) ); + viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) ); } ); afterEach( () => { @@ -338,7 +338,7 @@ describe( 'view', () => { it( 'should log warning when no selection', () => { const logSpy = testUtils.sinon.stub( log, 'warn' ); - viewDocument.selection.setTo( null ); + viewDocument.selection._setTo( null ); view.focus(); expect( logSpy.calledOnce ).to.be.true; diff --git a/tests/view/writer/wrap.js b/tests/view/writer/wrap.js index ed8408e94..b99995b4d 100644 --- a/tests/view/writer/wrap.js +++ b/tests/view/writer/wrap.js @@ -455,7 +455,7 @@ describe( 'Writer', () => { */ function test( input, wrapAttribute, expected ) { const { view, selection } = parse( input, { rootElement: viewRoot } ); - viewDocument.selection.setTo( selection ); + viewDocument.selection._setTo( selection ); const newPosition = writer.wrap( selection.getFirstRange(), parse( wrapAttribute ) ); From 142775fcc7d5344ca8cad0d11388ab906565b054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 10:21:49 +0100 Subject: [PATCH 29/89] Changed view selection setFake method as protected. Created view writer method to handle setting fake selection. --- .../model-selection-to-view-converters.js | 2 +- src/view/observer/fakeselectionobserver.js | 2 +- src/view/selection.js | 7 ++-- src/view/writer.js | 19 ++++++++++- .../model-selection-to-view-converters.js | 2 +- tests/view/manual/fakeselection.js | 4 +-- tests/view/observer/fakeselectionobserver.js | 6 ++-- tests/view/renderer.js | 28 ++++++++-------- tests/view/selection.js | 32 +++++++++---------- 9 files changed, 60 insertions(+), 42 deletions(-) diff --git a/src/conversion/model-selection-to-view-converters.js b/src/conversion/model-selection-to-view-converters.js index bae740b98..0de940fa2 100644 --- a/src/conversion/model-selection-to-view-converters.js +++ b/src/conversion/model-selection-to-view-converters.js @@ -129,5 +129,5 @@ export function clearAttributes() { * {@link module:engine/model/selection~Selection model selection} conversion. */ export function clearFakeSelection() { - return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection.setFake( false ); + return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection._setFake( false ); } diff --git a/src/view/observer/fakeselectionobserver.js b/src/view/observer/fakeselectionobserver.js index 2663bdac2..4e3302617 100644 --- a/src/view/observer/fakeselectionobserver.js +++ b/src/view/observer/fakeselectionobserver.js @@ -83,7 +83,7 @@ export default class FakeSelectionObserver extends Observer { _handleSelectionMove( keyCode ) { const selection = this.document.selection; const newSelection = new ViewSelection( selection ); - newSelection.setFake( false ); + newSelection._setFake( false ); // Left or up arrow pressed - move selection to start. if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) { diff --git a/src/view/selection.js b/src/view/selection.js index c75f6927d..6984545e4 100644 --- a/src/view/selection.js +++ b/src/view/selection.js @@ -109,12 +109,13 @@ export default class Selection { * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be * properly handled by screen readers). * + * @protected * @fires change * @param {Boolean} [value=true] If set to true selection will be marked as `fake`. * @param {Object} [options] Additional options. * @param {String} [options.label=''] Fake selection label. */ - setFake( value = true, options = {} ) { + _setFake( value = true, options = {} ) { this._isFake = value; this._fakeSelectionLabel = value ? options.label || '' : ''; @@ -124,7 +125,7 @@ export default class Selection { /** * Returns true if selection instance is marked as `fake`. * - * @see #setFake + * @see #_setFake * @returns {Boolean} */ get isFake() { @@ -134,7 +135,7 @@ export default class Selection { /** * Returns fake selection label. * - * @see #setFake + * @see #_setFake * @returns {String} */ get fakeSelectionLabel() { diff --git a/src/view/writer.js b/src/view/writer.js index f903026bc..80df24052 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -77,7 +77,24 @@ export default class Writer { * first parameter is a {@link module:engine/view/item~Item view item}. */ setSelectionFocus( itemOrPosition, offset ) { - this.model.document.selection._setFocus( itemOrPosition, offset ); + this.document.selection._setFocus( itemOrPosition, offset ); + } + + /** + * Sets {@link module:engine/view/selection~Selection selection's} to be marked as `fake`. A fake selection does + * not render as browser native selection over selected elements and is hidden to the user. + * This way, no native selection UI artifacts are displayed to the user and selection over elements can be + * represented in other way, for example by applying proper CSS class. + * + * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be + * properly handled by screen readers). + * + * @param {Boolean} [value=true] If set to true selection will be marked as `fake`. + * @param {Object} [options] Additional options. + * @param {String} [options.label=''] Fake selection label. + */ + setFakeSelection( value = true, options = {} ) { + this.document.selection._setFake( value, options ); } /** diff --git a/tests/conversion/model-selection-to-view-converters.js b/tests/conversion/model-selection-to-view-converters.js index e7c9e9859..28bfb45c4 100644 --- a/tests/conversion/model-selection-to-view-converters.js +++ b/tests/conversion/model-selection-to-view-converters.js @@ -480,7 +480,7 @@ describe( 'model-selection-to-view-converters', () => { dispatcher.on( 'selection', clearFakeSelection() ); view.change( writer => { - viewSelection.setFake( true ); + viewSelection._setFake( true ); dispatcher.convertSelection( docSelection, writer ); } ); expect( viewSelection.isFake ).to.be.false; diff --git a/tests/view/manual/fakeselection.js b/tests/view/manual/fakeselection.js index 59b15c47f..a045966f7 100644 --- a/tests/view/manual/fakeselection.js +++ b/tests/view/manual/fakeselection.js @@ -39,8 +39,8 @@ viewDocument.on( 'mouseup', ( evt, data ) => { console.log( 'Making selection around the .' ); const range = ViewRange.createOn( viewStrong ); - viewDocument.selection.setTo( [ range ] ); - viewDocument.selection.setFake( true, { label: 'fake selection over bar' } ); + viewDocument.selection._setTo( [ range ] ); + viewDocument.selection._setFake( true, { label: 'fake selection over bar' } ); viewDocument.render(); diff --git a/tests/view/observer/fakeselectionobserver.js b/tests/view/observer/fakeselectionobserver.js index cd8dba200..315148e46 100644 --- a/tests/view/observer/fakeselectionobserver.js +++ b/tests/view/observer/fakeselectionobserver.js @@ -33,7 +33,7 @@ describe( 'FakeSelectionObserver', () => { root = createViewRoot( viewDocument ); view.attachDomRoot( domRoot ); observer = view.getObserver( FakeSelectionObserver ); - viewDocument.selection.setFake(); + viewDocument.selection._setFake(); } ); afterEach( () => { @@ -41,7 +41,7 @@ describe( 'FakeSelectionObserver', () => { } ); it( 'should do nothing if selection is not fake', () => { - viewDocument.selection.setFake( false ); + viewDocument.selection._setFake( false ); return checkEventPrevention( keyCodes.arrowleft, false ); } ); @@ -200,7 +200,7 @@ describe( 'FakeSelectionObserver', () => { // // @param {Number} keyCode function changeFakeSelectionPressing( keyCode ) { - viewDocument.selection.setFake(); + viewDocument.selection._setFake(); const data = { keyCode, diff --git a/tests/view/renderer.js b/tests/view/renderer.js index 99849ca9c..f3ac45324 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -122,7 +122,7 @@ describe( 'Renderer', () => { renderer.markedChildren.clear(); selection._setTo( null ); - selection.setFake( false ); + selection._setFake( false ); selectionEditable = viewRoot; @@ -1369,7 +1369,7 @@ describe( 'Renderer', () => { it( 'should render fake selection', () => { const label = 'fake selection label'; - selection.setFake( true, { label } ); + selection._setFake( true, { label } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); @@ -1386,7 +1386,7 @@ describe( 'Renderer', () => { } ); it( 'should render   if no selection label is provided', () => { - selection.setFake( true ); + selection._setFake( true ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); @@ -1402,10 +1402,10 @@ describe( 'Renderer', () => { } ); it( 'should remove fake selection container when selection is no longer fake', () => { - selection.setFake( true ); + selection._setFake( true ); renderer.render(); - selection.setFake( false ); + selection._setFake( false ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 1 ); @@ -1421,14 +1421,14 @@ describe( 'Renderer', () => { it( 'should reuse fake selection container #1', () => { const label = 'fake selection label'; - selection.setFake( true, { label } ); + selection._setFake( true, { label } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); const container = domRoot.childNodes[ 1 ]; - selection.setFake( true, { label } ); + selection._setFake( true, { label } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); @@ -1442,19 +1442,19 @@ describe( 'Renderer', () => { } ); it( 'should reuse fake selection container #2', () => { - selection.setFake( true, { label: 'label 1' } ); + selection._setFake( true, { label: 'label 1' } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); const container = domRoot.childNodes[ 1 ]; - selection.setFake( false ); + selection._setFake( false ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 1 ); - selection.setFake( true, { label: 'label 2' } ); + selection._setFake( true, { label: 'label 2' } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); @@ -1468,14 +1468,14 @@ describe( 'Renderer', () => { } ); it( 'should reuse fake selection container #3', () => { - selection.setFake( true, { label: 'label 1' } ); + selection._setFake( true, { label: 'label 1' } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); const container = domRoot.childNodes[ 1 ]; - selection.setFake( true, { label: 'label 2' } ); + selection._setFake( true, { label: 'label 2' } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); @@ -1489,7 +1489,7 @@ describe( 'Renderer', () => { } ); it( 'should style fake selection container properly', () => { - selection.setFake( true, { label: 'fake selection' } ); + selection._setFake( true, { label: 'fake selection' } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); @@ -1502,7 +1502,7 @@ describe( 'Renderer', () => { } ); it( 'should bind fake selection container to view selection', () => { - selection.setFake( true, { label: 'fake selection' } ); + selection._setFake( true, { label: 'fake selection' } ); renderer.render(); expect( domRoot.childNodes.length ).to.equal( 2 ); diff --git a/tests/view/selection.js b/tests/view/selection.js index 5611539d9..f3a162791 100644 --- a/tests/view/selection.js +++ b/tests/view/selection.js @@ -94,7 +94,7 @@ describe( 'Selection', () => { it( 'should be able to create a fake selection from the other fake selection', () => { const otherSelection = new Selection( [ range2, range3 ], true ); - otherSelection.setFake( true, { label: 'foo bar baz' } ); + otherSelection._setFake( true, { label: 'foo bar baz' } ); const selection = new Selection( otherSelection ); expect( selection.isFake ).to.be.true; @@ -511,15 +511,15 @@ describe( 'Selection', () => { it( 'should return false if one selection is fake', () => { const otherSelection = new Selection(); - otherSelection.setFake( true ); + otherSelection._setFake( true ); expect( selection.isEqual( otherSelection ) ).to.be.false; } ); it( 'should return true if both selection are fake', () => { const otherSelection = new Selection( [ range1 ] ); - otherSelection.setFake( true ); - selection.setFake( true ); + otherSelection._setFake( true ); + selection._setFake( true ); selection._setTo( range1 ); expect( selection.isEqual( otherSelection ) ).to.be.true; @@ -527,8 +527,8 @@ describe( 'Selection', () => { it( 'should return false if both selection are fake but have different label', () => { const otherSelection = new Selection( [ range1 ] ); - otherSelection.setFake( true, { label: 'foo bar baz' } ); - selection.setFake( true ); + otherSelection._setFake( true, { label: 'foo bar baz' } ); + selection._setFake( true ); selection._setTo( range1 ); expect( selection.isEqual( otherSelection ) ).to.be.false; @@ -709,7 +709,7 @@ describe( 'Selection', () => { it( 'should set fake state and label', () => { const otherSelection = new Selection(); const label = 'foo bar baz'; - otherSelection.setFake( true, { label } ); + otherSelection._setFake( true, { label } ); selection._setTo( otherSelection ); expect( selection.isFake ).to.be.true; @@ -863,7 +863,7 @@ describe( 'Selection', () => { } ); } ); - describe( 'getEditableElement', () => { + describe( 'getEditableElement()', () => { it( 'should return null if no ranges in selection', () => { expect( selection.editableElement ).to.be.null; } ); @@ -893,31 +893,31 @@ describe( 'Selection', () => { } ); } ); - describe( 'setFake', () => { + describe( '_setFake()', () => { it( 'should allow to set selection to fake', () => { - selection.setFake( true ); + selection._setFake( true ); expect( selection.isFake ).to.be.true; } ); it( 'should allow to set fake selection label', () => { const label = 'foo bar baz'; - selection.setFake( true, { label } ); + selection._setFake( true, { label } ); expect( selection.fakeSelectionLabel ).to.equal( label ); } ); it( 'should not set label when set to false', () => { const label = 'foo bar baz'; - selection.setFake( false, { label } ); + selection._setFake( false, { label } ); expect( selection.fakeSelectionLabel ).to.equal( '' ); } ); it( 'should reset label when set to false', () => { const label = 'foo bar baz'; - selection.setFake( true, { label } ); - selection.setFake( false ); + selection._setFake( true, { label } ); + selection._setFake( false ); expect( selection.fakeSelectionLabel ).to.equal( '' ); } ); @@ -930,11 +930,11 @@ describe( 'Selection', () => { done(); } ); - selection.setFake( true, { label: 'foo bar baz' } ); + selection._setFake( true, { label: 'foo bar baz' } ); } ); } ); - describe( 'getSelectedElement', () => { + describe( 'getSelectedElement()', () => { it( 'should return selected element', () => { const { selection, view } = parse( 'foo [bar] baz' ); const b = view.getChild( 1 ); From f427bb233cceeb47329b21ceebc7ab255204111f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 12:54:41 +0100 Subject: [PATCH 30/89] Removed view selection from conversionApi, it can be accessed by writer now. --- src/controller/editingcontroller.js | 3 +-- .../model-selection-to-view-converters.js | 16 ++++++++++------ src/conversion/model-to-view-converters.js | 6 ++++-- src/dev-utils/model.js | 11 +++++------ 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index 0ddbd046a..60b1961fd 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -79,8 +79,7 @@ export default class EditingController { * @member {module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher} #modelToView */ this.modelToView = new ModelConversionDispatcher( this.model, { - mapper: this.mapper, - viewSelection: this.view.document.selection + mapper: this.mapper } ); const doc = this.model.document; diff --git a/src/conversion/model-selection-to-view-converters.js b/src/conversion/model-selection-to-view-converters.js index 0de940fa2..78edc8f59 100644 --- a/src/conversion/model-selection-to-view-converters.js +++ b/src/conversion/model-selection-to-view-converters.js @@ -39,7 +39,7 @@ export function convertRangeSelection() { viewRanges.push( viewRange ); } - conversionApi.viewSelection._setTo( viewRanges, selection.isBackward ); + conversionApi.writer.setSelection( viewRanges, selection.isBackward ); }; } @@ -77,11 +77,12 @@ export function convertCollapsedSelection() { return; } + const writer = conversionApi.writer; const modelPosition = selection.getFirstPosition(); const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); - const brokenPosition = conversionApi.writer.breakAttributes( viewPosition ); + const brokenPosition = writer.breakAttributes( viewPosition ); - conversionApi.viewSelection._setTo( brokenPosition ); + writer.setSelection( brokenPosition ); }; } @@ -111,7 +112,10 @@ export function convertCollapsedSelection() { */ export function clearAttributes() { return ( evt, data, consumable, conversionApi ) => { - for ( const range of conversionApi.viewSelection.getRanges() ) { + const writer = conversionApi.writer; + const viewSelection = writer.document.selection; + + for ( const range of viewSelection.getRanges() ) { // Not collapsed selection should not have artifacts. if ( range.isCollapsed ) { // Position might be in the node removed by the view writer. @@ -120,7 +124,7 @@ export function clearAttributes() { } } } - conversionApi.viewSelection._setTo( null ); + writer.setSelection( null ); }; } @@ -129,5 +133,5 @@ export function clearAttributes() { * {@link module:engine/model/selection~Selection model selection} conversion. */ export function clearFakeSelection() { - return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection._setFake( false ); + return ( evt, data, consumable, conversionApi ) => conversionApi.writer.setFakeSelection( false ); } diff --git a/src/conversion/model-to-view-converters.js b/src/conversion/model-to-view-converters.js index 84d71b80d..b2b4d57ec 100644 --- a/src/conversion/model-to-view-converters.js +++ b/src/conversion/model-to-view-converters.js @@ -332,10 +332,11 @@ export function wrap( elementCreator ) { } const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) { // Selection attribute conversion. - viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), newViewElement, conversionApi.viewSelection ); + viewWriter.wrap( viewSelection.getFirstRange(), newViewElement, viewSelection ); } else { // Node attribute conversion. let viewRange = conversionApi.mapper.toViewRange( data.range ); @@ -389,9 +390,10 @@ export function highlightText( highlightDescriptor ) { const viewElement = createViewElementFromHighlightDescriptor( descriptor ); const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) { - viewWriter.wrap( conversionApi.viewSelection.getFirstRange(), viewElement, conversionApi.viewSelection ); + viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection ); } else { const viewRange = conversionApi.mapper.toViewRange( data.range ); viewWriter.wrap( viewRange, viewElement ); diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 03750a04f..897678cd9 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -21,10 +21,8 @@ import ModelSelection from '../model/selection'; import ModelDocumentFragment from '../model/documentfragment'; import DocumentSelection from '../model/documentselection'; -import ViewWriter from '../view/writer'; -import ViewDocument from '../view/document'; +import View from '../view/view'; import ViewConversionDispatcher from '../conversion/viewconversiondispatcher'; -import ViewSelection from '../view/selection'; import ViewDocumentFragment from '../view/documentfragment'; import ViewContainerElement from '../view/containerelement'; import ViewAttributeElement from '../view/attributeelement'; @@ -194,8 +192,9 @@ export function stringify( node, selectionOrPositionOrRange = null ) { // Setup model to view converter. const viewDocumentFragment = new ViewDocumentFragment(); - const viewSelection = new ViewSelection(); - const modelToView = new ModelConversionDispatcher( model, { mapper, viewSelection } ); + const view = new View(); + const viewSelection = view.document.selection; + const modelToView = new ModelConversionDispatcher( model, { mapper } ); // Bind root elements. mapper.bindElements( node.root, viewDocumentFragment ); @@ -216,7 +215,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) { modelToView.on( 'selection', convertCollapsedSelection() ); // Convert model to view. - const writer = new ViewWriter( new ViewDocument() ); + const writer = view._writer; modelToView.convertInsert( range, writer ); // Convert model selection to view selection. From f91150ea6604b26f40f13ed12a7b27c7fa1dbfb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 14:17:44 +0100 Subject: [PATCH 31/89] Removed view selection parameter from view writer.wrap() method. --- src/view/writer.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/view/writer.js b/src/view/writer.js index 80df24052..95c0fb7a9 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -630,11 +630,9 @@ export default class Writer { * * @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 - * wrapping collapsed range. * @returns {module:engine/view/range~Range} range Range after wrapping, spanning over wrapping attribute element. */ - wrap( range, attribute, viewSelection = null ) { + wrap( range, attribute ) { if ( !( attribute instanceof AttributeElement ) ) { throw new CKEditorError( 'view-writer-wrap-invalid-attribute' ); } @@ -653,10 +651,11 @@ export default class Writer { } position = this._wrapPosition( position, attribute ); + const viewSelection = this.document.selection; // If wrapping position is equal to view selection, move view selection inside wrapping attribute element. - if ( viewSelection && viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) { - viewSelection._setTo( position ); + if ( viewSelection.isCollapsed && viewSelection.getFirstPosition().isEqual( range.start ) ) { + this.setSelection( position ); } return new Range( position ); From 9662257fe7339f8c5b3ca91b979b5eab58e0c7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 14:22:36 +0100 Subject: [PATCH 32/89] Using view.change in dev-utils tests instead of modifing selection directly. --- tests/dev-utils/view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/dev-utils/view.js b/tests/dev-utils/view.js index 3e516dc60..8e2658a06 100644 --- a/tests/dev-utils/view.js +++ b/tests/dev-utils/view.js @@ -63,7 +63,9 @@ describe( 'view test utils', () => { const root = createAttachedRoot( viewDocument, element ); root.appendChildren( new Element( 'p' ) ); - viewDocument.selection._setTo( Range.createFromParentsAndOffsets( root, 0, root, 1 ) ); + view.change( writer => { + writer.setSelection( Range.createFromParentsAndOffsets( root, 0, root, 1 ) ); + } ); expect( getData( view, options ) ).to.equal( '[

]' ); sinon.assert.calledOnce( stringifySpy ); From 69f154fe7de4176113881c961b6a89d63b4b8b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 14:26:46 +0100 Subject: [PATCH 33/89] Using view.change in placeholder tests instead of modifing selection directly. --- tests/view/placeholder.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js index adbecdf7e..1ccf28465 100644 --- a/tests/view/placeholder.js +++ b/tests/view/placeholder.js @@ -92,8 +92,8 @@ describe( 'placeholder', () => { expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; - view.change( () => { - viewDocument.selection._setTo( [ ViewRange.createIn( element ) ] ); + view.change( writer => { + writer.setSelection( ViewRange.createIn( element ) ); } ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; @@ -141,12 +141,12 @@ describe( 'placeholder', () => { expect( secondElement.hasClass( 'ck-placeholder' ) ).to.be.true; // Move selection to the elements with placeholders. - view.change( () => { - viewDocument.selection._setTo( [ ViewRange.createIn( element ) ] ); + view.change( writer => { + writer.setSelection( ViewRange.createIn( element ) ); } ); - secondView.change( () => { - secondDocument.selection._setTo( [ ViewRange.createIn( secondElement ) ] ); + secondView.change( writer => { + writer.setSelection( ViewRange.createIn( secondElement ) ); } ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'first placeholder' ); @@ -162,8 +162,8 @@ describe( 'placeholder', () => { attachPlaceholder( view, element, 'foo bar baz' ); - view.change( () => { - viewDocument.selection._setTo( ViewRange.createIn( element ) ); + view.change( writer => { + writer.setSelection( ViewRange.createIn( element ) ); // Here we are before rendering - placeholder is visible in first element; expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); From f5b9b68d570bac58967bd7e3e5715c5385a68e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 14:41:48 +0100 Subject: [PATCH 34/89] Updated fake selection manual test to work with view changes. --- tests/view/manual/fakeselection.js | 39 ++++++++++++++++-------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/tests/view/manual/fakeselection.js b/tests/view/manual/fakeselection.js index a045966f7..68738afc2 100644 --- a/tests/view/manual/fakeselection.js +++ b/tests/view/manual/fakeselection.js @@ -5,21 +5,22 @@ /* globals document, console */ -import ViewDocument from '../../../src/view/document'; +import View from '../../../src/view/view'; import DomEventObserver from '../../../src/view/observer/domeventobserver'; import ViewRange from '../../../src/view/range'; import createViewRoot from '../_utils/createroot'; import { setData } from '../../../src/dev-utils/view'; -const viewDocument = new ViewDocument(); +const view = new View(); +const viewDocument = view.document; const domEditable = document.getElementById( 'editor' ); -const viewRoot = createViewRoot(); +const viewRoot = createViewRoot( viewDocument ); let viewStrong; -viewDocument.attachDomRoot( domEditable ); +view.attachDomRoot( domEditable ); // Add mouseup oberver. -viewDocument.addObserver( class extends DomEventObserver { +view.addObserver( class extends DomEventObserver { get domEventType() { return [ 'mousedown', 'mouseup' ]; } @@ -30,19 +31,19 @@ viewDocument.addObserver( class extends DomEventObserver { } ); viewDocument.on( 'selectionChange', ( evt, data ) => { - viewDocument.selection.setTo( data.newSelection ); - viewDocument.render(); + view.change( writer => { + writer.setSelection( data.newSelection ); + } ); } ); viewDocument.on( 'mouseup', ( evt, data ) => { if ( data.target == viewStrong ) { console.log( 'Making selection around the .' ); - const range = ViewRange.createOn( viewStrong ); - viewDocument.selection._setTo( [ range ] ); - viewDocument.selection._setFake( true, { label: 'fake selection over bar' } ); - - viewDocument.render(); + view.change( writer => { + writer.setSelection( ViewRange.createOn( viewStrong ) ); + writer.setFakeSelection( true, { label: 'fake selection over bar' } ); + } ); data.preventDefault(); } @@ -64,21 +65,23 @@ viewDocument.selection.on( 'change', () => { } ); viewDocument.on( 'focus', () => { - viewStrong.addClass( 'focused' ); - viewDocument.render(); + view.change( () => { + viewStrong.addClass( 'focused' ); + } ); console.log( 'The document was focused.' ); } ); viewDocument.on( 'blur', () => { - viewStrong.removeClass( 'focused' ); - viewDocument.render(); + view.change( () => { + viewStrong.removeClass( 'focused' ); + } ); console.log( 'The document was blurred.' ); } ); -setData( viewDocument, '{}foobarbaz' ); +setData( view, '{}foobarbaz' ); const viewP = viewRoot.getChild( 0 ); viewStrong = viewP.getChild( 1 ); -viewDocument.focus(); +view.focus(); From 72356b49d8cde15ab25058090148ecde1a784117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 14:52:06 +0100 Subject: [PATCH 35/89] Using view.change in focusobserver tests instead of modifing selection directly. --- tests/view/observer/focusobserver.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/view/observer/focusobserver.js b/tests/view/observer/focusobserver.js index d28018646..6a0bfc4c4 100644 --- a/tests/view/observer/focusobserver.js +++ b/tests/view/observer/focusobserver.js @@ -6,7 +6,6 @@ /* globals document */ import FocusObserver from '../../../src/view/observer/focusobserver'; import View from '../../../src/view/view'; -import ViewRange from '../../../src/view/range'; import createViewRoot from '../_utils/createroot'; import { setData } from '../../../src/dev-utils/view'; @@ -95,7 +94,9 @@ describe( 'FocusObserver', () => { } ); it( 'should set isFocused to false on blur when selection in same editable', () => { - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) ); + view.change( writer => { + writer.setSelection( viewMain, 0 ); + } ); observer.onDomEvent( { type: 'focus', target: domMain } ); @@ -107,7 +108,9 @@ describe( 'FocusObserver', () => { } ); it( 'should not set isFocused to false on blur when it is fired on other editable', () => { - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ) ); + view.change( writer => { + writer.setSelection( viewMain, 0 ); + } ); observer.onDomEvent( { type: 'focus', target: domMain } ); From 9feacdf5b92396c80f99e4f0c74ef5c052d064e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 15:08:28 +0100 Subject: [PATCH 36/89] Using view.change in mutationobserver tests instead of modifing selection directly. --- tests/view/observer/mutationobserver.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/view/observer/mutationobserver.js b/tests/view/observer/mutationobserver.js index 5dd0f9949..d7aef2bfa 100644 --- a/tests/view/observer/mutationobserver.js +++ b/tests/view/observer/mutationobserver.js @@ -227,10 +227,10 @@ describe( 'MutationObserver', () => { it( 'should fire children mutation if the mutation occurred in the inline filler', () => { const { view: viewContainer, selection } = parse( 'foo[]bar' ); - viewRoot.appendChildren( viewContainer ); - viewDocument.selection._setTo( selection ); - - view.render(); + view.change( writer => { + viewRoot.appendChildren( viewContainer ); + writer.setSelection( selection ); + } ); const inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ]; inlineFiller.data += 'x'; @@ -245,17 +245,18 @@ describe( 'MutationObserver', () => { it( 'should have no inline filler in mutation', () => { const { view: viewContainer, selection } = parse( 'foo[]bar' ); - viewRoot.appendChildren( viewContainer ); - viewDocument.selection._setTo( selection ); - - view.render(); + view.change( writer => { + viewRoot.appendChildren( viewContainer ); + writer.setSelection( selection ); + } ); let inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ]; inlineFiller.data += 'x'; - viewContainer.getChild( 1 ).appendChildren( parse( 'x' ) ); - mutationObserver.flush(); - view.render(); + view.change( () => { + viewContainer.getChild( 1 ).appendChildren( parse( 'x' ) ); + mutationObserver.flush(); + } ); inlineFiller = domEditor.childNodes[ 2 ].childNodes[ 1 ].childNodes[ 0 ]; inlineFiller.data += 'y'; From 9435e48cc2384fa7655e23e90222cef4ce039c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 15:16:59 +0100 Subject: [PATCH 37/89] Using view.change in selectionobserver tests instead of modifing selection directly. --- tests/view/observer/selectionobserver.js | 38 +++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/tests/view/observer/selectionobserver.js b/tests/view/observer/selectionobserver.js index 69b2dd00b..86b1b59df 100644 --- a/tests/view/observer/selectionobserver.js +++ b/tests/view/observer/selectionobserver.js @@ -36,17 +36,17 @@ describe( 'SelectionObserver', () => { viewRoot = viewDocument.getRoot(); - viewRoot.appendChildren( parse( - 'xxx' + - 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy' ) ); + view.change( writer => { + viewRoot.appendChildren( parse( + 'xxx' + + 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy' ) ); - view.render(); + writer.setSelection( null ); + domDocument.getSelection().removeAllRanges(); - viewDocument.selection._setTo( null ); - domDocument.getSelection().removeAllRanges(); - - viewDocument.isFocused = true; - domMain.focus(); + viewDocument.isFocused = true; + domMain.focus(); + } ); selectionObserver.enable(); @@ -104,8 +104,10 @@ describe( 'SelectionObserver', () => { setTimeout( done, 70 ); const viewBar = viewDocument.getRoot().getChild( 1 ).getChild( 0 ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) ); - view.render(); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( viewBar, 1, viewBar, 2 ) ); + } ); } ); it( 'should not fired if observer is disabled', done => { @@ -163,7 +165,9 @@ describe( 'SelectionObserver', () => { let counter = 70; const viewFoo = viewDocument.getRoot().getChild( 0 ).getChild( 0 ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewFoo, 0, viewFoo, 0 ) ); + view.change( writer => { + writer.setSelection( viewFoo, 0 ); + } ); return new Promise( ( resolve, reject ) => { testUtils.sinon.stub( log, 'warn' ).callsFake( msg => { @@ -315,15 +319,13 @@ describe( 'SelectionObserver', () => { viewDocument.on( 'selectionChange', () => { // Manually set selection because no handlers are set for selectionChange event in this test. // Normally this is handled by view -> model -> view selection converters chain. - const viewSel = viewDocument.selection; - const viewAnchor = view.domConverter.domPositionToView( sel.anchorNode, sel.anchorOffset ); const viewFocus = view.domConverter.domPositionToView( sel.focusNode, sel.focusOffset ); - viewSel._setTo( viewAnchor ); - viewSel._setFocus( viewFocus ); - - view.render(); + view.change( writer => { + writer.setSelection( viewAnchor ); + writer.setSelectionFocus( viewFocus ); + } ); } ); viewDocument.once( 'selectionChange', () => { From 8a00bcbc9ed84901c6f3aeaf96095724ce026163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 30 Jan 2018 15:41:02 +0100 Subject: [PATCH 38/89] More test updated to use view writer.change() block. --- tests/view/view/jumpoverinlinefiller.js | 40 +++++++------- tests/view/view/jumpoveruielement.js | 69 +++++++++++++++++++------ tests/view/view/view.js | 13 +++-- 3 files changed, 84 insertions(+), 38 deletions(-) diff --git a/tests/view/view/jumpoverinlinefiller.js b/tests/view/view/jumpoverinlinefiller.js index 47837df1c..6792b0e88 100644 --- a/tests/view/view/jumpoverinlinefiller.js +++ b/tests/view/view/jumpoverinlinefiller.js @@ -5,7 +5,6 @@ /* globals document */ -import ViewRange from '../../../src/view/range'; import View from '../../../src/view/view'; import { INLINE_FILLER_LENGTH, isInlineFiller, startsWithFiller } from '../../../src/view/filler'; @@ -105,27 +104,28 @@ describe( 'View', () => { // } ); it( 'should do nothing if caret is not directly before the filler', () => { - setData( view, 'foo[]bar' ); - view.render(); - - // Insert a letter to the : 'foox{}bar' - // Do this both in the view and in the DOM to simulate typing and to avoid rendering (which would remove the filler). - const viewB = viewDocument.selection.getFirstPosition().parent; - const viewTextX = parse( 'x' ); - viewB.appendChildren( viewTextX ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewTextX, 1, viewTextX, 1 ) ); + view.change( () => { + setData( view, 'foo[]bar' ); + } ); - const domB = view.getDomRoot( 'main' ).querySelector( 'b' ); const domSelection = document.getSelection(); - domB.childNodes[ 0 ].data += 'x'; - - const domRange = document.createRange(); - domSelection.removeAllRanges(); - domRange.setStart( domB.childNodes[ 0 ], INLINE_FILLER_LENGTH + 1 ); - domRange.collapse( true ); - domSelection.addRange( domRange ); - - view.render(); + view.change( writer => { + // Insert a letter to the : 'foox{}bar' + // Do this both in the view and in the DOM to simulate typing and to avoid rendering (which would remove the filler). + const viewB = writer.document.selection.getFirstPosition().parent; + const viewTextX = parse( 'x' ); + viewB.appendChildren( viewTextX ); + writer.setSelection( viewTextX, 1 ); + + const domB = view.getDomRoot( 'main' ).querySelector( 'b' ); + domB.childNodes[ 0 ].data += 'x'; + + const domRange = document.createRange(); + domSelection.removeAllRanges(); + domRange.setStart( domB.childNodes[ 0 ], INLINE_FILLER_LENGTH + 1 ); + domRange.collapse( true ); + domSelection.addRange( domRange ); + } ); viewDocument.fire( 'keydown', { keyCode: keyCodes.arrowleft, domTarget: view.domRoots.get( 'main' ) } ); diff --git a/tests/view/view/jumpoveruielement.js b/tests/view/view/jumpoveruielement.js index fc67cd9ef..fde4aeee4 100644 --- a/tests/view/view/jumpoveruielement.js +++ b/tests/view/view/jumpoveruielement.js @@ -90,7 +90,10 @@ describe( 'View', () => { // fooxxx{}bar const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] ); + + view.change( writer => { + writer.setSelection( [ ViewRange.createFromParentsAndOffsets( bar, 0, bar, 0 ) ] ); + } ); renderAndFireKeydownEvent( { keyCode: keyCodes.arrowleft } ); @@ -105,7 +108,10 @@ describe( 'View', () => { // foo[]xxxbar const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] ); + + view.change( writer => { + writer.setSelection( [ ViewRange.createFromParentsAndOffsets( p, 1, p, 1 ) ] ); + } ); renderAndFireKeydownEvent(); @@ -122,7 +128,10 @@ describe( 'View', () => { // foo{}xxxbar const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + + view.change( writer => { + writer.setSelection( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + } ); renderAndFireKeydownEvent(); @@ -139,7 +148,10 @@ describe( 'View', () => { // foo{}xxxyyybar' const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + + view.change( writer => { + writer.setSelection( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + } ); renderAndFireKeydownEvent(); @@ -156,9 +168,12 @@ describe( 'View', () => { // foo{}xxxyyy const p = new ViewContainerElement( 'p', null, [ foo, ui, ui2 ] ); const div = new ViewContainerElement( 'div' ); - viewRoot.appendChildren( p ); - viewRoot.appendChildren( div ); - viewDocument.selection._setTo( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + + view.change( writer => { + viewRoot.appendChildren( p ); + viewRoot.appendChildren( div ); + writer.setSelection( [ ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ] ); + } ); renderAndFireKeydownEvent(); @@ -176,7 +191,10 @@ describe( 'View', () => { const b = new ViewAttribtueElement( 'b', null, foo ); const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + } ); renderAndFireKeydownEvent(); @@ -194,7 +212,10 @@ describe( 'View', () => { const b = new ViewAttribtueElement( 'b', null, foo ); const p = new ViewContainerElement( 'p', null, [ b, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( b, 1, b, 1 ) ); + } ); renderAndFireKeydownEvent(); @@ -222,7 +243,10 @@ describe( 'View', () => { const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + } ); renderAndFireKeydownEvent(); @@ -249,7 +273,10 @@ describe( 'View', () => { const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + } ); renderAndFireKeydownEvent(); @@ -277,7 +304,10 @@ describe( 'View', () => { const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 3, foo, 3 ) ); + } ); renderAndFireKeydownEvent( { shiftKey: true } ); @@ -353,7 +383,10 @@ describe( 'View', () => { const p = new ViewContainerElement( 'p', null, [ foo, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + } ); renderAndFireKeydownEvent( { shiftKey: true } ); @@ -378,7 +411,10 @@ describe( 'View', () => { const i = new ViewAttribtueElement( 'i', null, b ); const p = new ViewContainerElement( 'p', null, [ i, ui, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + } ); renderAndFireKeydownEvent( { shiftKey: true } ); @@ -404,7 +440,10 @@ describe( 'View', () => { const b2 = new ViewAttribtueElement( 'b' ); const p = new ViewContainerElement( 'p', null, [ foo, b1, ui, ui2, b2, bar ] ); viewRoot.appendChildren( p ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( foo, 2, foo, 3 ) ); + } ); renderAndFireKeydownEvent( { shiftKey: true } ); diff --git a/tests/view/view/view.js b/tests/view/view/view.js index 5d9579d68..c15fd3599 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -255,7 +255,9 @@ describe( 'view', () => { left: '-1000px' } ); - viewDocument.selection._setTo( range ); + view.change( writer => { + writer.setSelection( range ); + } ); view.scrollToTheSelection(); sinon.assert.calledWithMatch( stub, sinon.match.number, sinon.match.number ); @@ -301,7 +303,10 @@ describe( 'view', () => { document.body.appendChild( domEditable ); viewEditable = createViewRoot( viewDocument, 'div', 'main' ); view.attachDomRoot( domEditable ); - viewDocument.selection._setTo( ViewRange.createFromParentsAndOffsets( viewEditable, 0, viewEditable, 0 ) ); + + view.change( writer => { + writer.setSelection( viewEditable, 0 ); + } ); } ); afterEach( () => { @@ -338,7 +343,9 @@ describe( 'view', () => { it( 'should log warning when no selection', () => { const logSpy = testUtils.sinon.stub( log, 'warn' ); - viewDocument.selection._setTo( null ); + view.change( writer => { + writer.setSelection( null ); + } ); view.focus(); expect( logSpy.calledOnce ).to.be.true; From 4ed39d2e64e7afdb84f3b4de8a5f126c7b58700e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 31 Jan 2018 09:28:03 +0100 Subject: [PATCH 39/89] Using writer to set fake selection in model selection to view converter tests. --- tests/conversion/model-selection-to-view-converters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conversion/model-selection-to-view-converters.js b/tests/conversion/model-selection-to-view-converters.js index 28bfb45c4..f867e3d25 100644 --- a/tests/conversion/model-selection-to-view-converters.js +++ b/tests/conversion/model-selection-to-view-converters.js @@ -480,7 +480,7 @@ describe( 'model-selection-to-view-converters', () => { dispatcher.on( 'selection', clearFakeSelection() ); view.change( writer => { - viewSelection._setFake( true ); + writer.setFakeSelection( true ); dispatcher.convertSelection( docSelection, writer ); } ); expect( viewSelection.isFake ).to.be.false; From 9de3ace6a12cb7abfabf99e7c0e083be8861784e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 5 Feb 2018 18:27:29 +0100 Subject: [PATCH 40/89] Align manual tests to latest changes in the view. --- src/dev-utils/view.js | 11 ++++--- src/view/editableelement.js | 7 ++-- tests/manual/tickets/880/1.js | 2 +- tests/view/manual/clickobserver.js | 33 +++++++++---------- tests/view/manual/focus.js | 44 +++++++++++++++++--------- tests/view/manual/focusobserver.js | 31 ++++++++++++------ tests/view/manual/immutable.js | 36 +++++++++++++++------ tests/view/manual/inline-filler.js | 36 +++++++++++++-------- tests/view/manual/keyobserver.js | 21 ++++++++---- tests/view/manual/mutationobserver.js | 25 +++++++++------ tests/view/manual/noselection.js | 19 +++++++---- tests/view/manual/selectionobserver.js | 17 +++++----- tests/view/manual/uielement.js | 27 ++++++++-------- tests/view/manual/x-index.js | 20 ++++++------ 14 files changed, 203 insertions(+), 126 deletions(-) diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js index ab5f78ba4..5f6f3c816 100644 --- a/src/dev-utils/view.js +++ b/src/dev-utils/view.js @@ -89,11 +89,14 @@ export function setData( view, data, options = {} ) { const document = view.document; const rootName = options.rootName || 'main'; const root = document.getRoot( rootName ); - const result = setData._parse( data, { rootElement: root } ); - if ( result.view && result.selection ) { - document.selection._setTo( result.selection ); - } + view.change( writer => { + const result = setData._parse( data, { rootElement: root } ); + + if ( result.view && result.selection ) { + writer.setSelection( result.selection ); + } + } ); } // Set parse as setData private method - needed for testing/spying. diff --git a/src/view/editableelement.js b/src/view/editableelement.js index 9682efd09..0fab0aebc 100644 --- a/src/view/editableelement.js +++ b/src/view/editableelement.js @@ -84,11 +84,10 @@ export default class EditableElement extends ContainerElement { isFocused => isFocused && document.selection.editableElement == this ); - // Update focus state before each rendering. Rendering should not change neither the selection nor the value of - // document.isFocused property. - this.listenTo( document, 'render', () => { + // Update focus state based on selection changes. + this.listenTo( document.selection, 'change', () => { this.isFocused = document.isFocused && document.selection.editableElement == this; - }, { priority: 'high' } ); + } ); } } diff --git a/tests/manual/tickets/880/1.js b/tests/manual/tickets/880/1.js index f95ceb0b0..0e61d47f0 100644 --- a/tests/manual/tickets/880/1.js +++ b/tests/manual/tickets/880/1.js @@ -18,7 +18,7 @@ ClassicEditor .then( editor => { window.editor = editor; - editor.editing.view.on( 'selectionChange', () => { + editor.editing.view.document.on( 'selectionChange', () => { editor.model.change( () => { } ); console.log( 'selectionChange', ( new Date() ).getTime() ); diff --git a/tests/view/manual/clickobserver.js b/tests/view/manual/clickobserver.js index a1124a875..255421d38 100644 --- a/tests/view/manual/clickobserver.js +++ b/tests/view/manual/clickobserver.js @@ -5,15 +5,19 @@ /* globals console, document */ -import Document from '../../../src/view/document'; +import View from '../../../src/view/view'; import DomEventObserver from '../../../src/view/observer/domeventobserver'; import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); +const view = new View(); +const viewDocument = view.document; + +// Disable rendering for this example, because it re-enables all observers each time view is rendered. +view.render = () => {}; class ClickObserver1 extends DomEventObserver { - constructor( viewDocument ) { - super( viewDocument ); + constructor( view ) { + super( view ); this.id = 1; this.domEventType = 'click'; @@ -25,8 +29,8 @@ class ClickObserver1 extends DomEventObserver { } class ClickObserver2 extends DomEventObserver { - constructor( viewDocument ) { - super( viewDocument ); + constructor( view ) { + super( view ); this.id = 2; this.domEventType = 'click'; @@ -37,19 +41,16 @@ class ClickObserver2 extends DomEventObserver { } } -const observer1 = new ClickObserver1( viewDocument ); - viewDocument.on( 'click', ( evt, evtData ) => console.log( 'click', evtData.id, evtData.domTarget.id ) ); -document.getElementById( 'enable1' ).addEventListener( 'click', () => observer1.enable() ); -document.getElementById( 'disable1' ).addEventListener( 'click', () => observer1.disable() ); // Random order. -viewDocument.addObserver( ClickObserver1 ); - +view.addObserver( ClickObserver1 ); createViewRoot( viewDocument, 'div', 'clickerA' ); -viewDocument.attachDomRoot( document.getElementById( 'clickerA' ), 'clickerA' ); - -viewDocument.addObserver( ClickObserver2 ); +view.attachDomRoot( document.getElementById( 'clickerA' ), 'clickerA' ); +view.addObserver( ClickObserver2 ); createViewRoot( viewDocument, 'div', 'clickerB' ); -viewDocument.attachDomRoot( document.getElementById( 'clickerB' ), 'clickerB' ); +view.attachDomRoot( document.getElementById( 'clickerB' ), 'clickerB' ); + +document.getElementById( 'enable1' ).addEventListener( 'click', () => view.getObserver( ClickObserver1 ).enable() ); +document.getElementById( 'disable1' ).addEventListener( 'click', () => view.getObserver( ClickObserver1 ).disable() ); diff --git a/tests/view/manual/focus.js b/tests/view/manual/focus.js index c1945a198..1362634e8 100644 --- a/tests/view/manual/focus.js +++ b/tests/view/manual/focus.js @@ -5,32 +5,46 @@ /* globals document */ -import Document from '../../../src/view/document'; -import { parse } from '../../../src/dev-utils/view'; +import View from '../../../src/view/view'; +import ViewPosition from '../../../src/view/position'; +import ViewRange from '../../../src/view/range'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); +const view = new View(); +const viewDocument = view.document; const domEditable1 = document.getElementById( 'editable1' ); const domEditable2 = document.getElementById( 'editable2' ); -const editable1 = viewDocument.createRoot( domEditable1, 'editable1' ); -const editable2 = viewDocument.createRoot( domEditable2, 'editable2' ); +const editable1 = createViewRoot( viewDocument, 'div', 'editable1' ); +view.attachDomRoot( domEditable1, 'editable1' ); -viewDocument.on( 'selectionChange', ( evt, data ) => { - viewDocument.selection.setTo( data.newSelection ); -} ); +const editable2 = createViewRoot( viewDocument, 'div', 'editable2' ); +view.attachDomRoot( domEditable2, 'editable2' ); + +let text1, text2; + +view.change( writer => { + text1 = writer.createText( 'Foo bar baz' ); + text2 = writer.createText( 'Foo bar baz' ); -const { selection: selection1 } = parse( '

Foo {bar} baz.

', { rootElement: editable1 } ); -const { selection: selection2 } = parse( '

{Foo} bar baz.

', { rootElement: editable2 } ); + writer.insert( ViewPosition.createAt( editable1 ), text1 ); + writer.insert( ViewPosition.createAt( editable2 ), text2 ); +} ); document.getElementById( 'button1' ).addEventListener( 'click', () => { - viewDocument.selection.setTo( selection1 ); - viewDocument.focus(); + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( text1, 4, text1, 7 ) ); + } ); + + view.focus(); } ); document.getElementById( 'button2' ).addEventListener( 'click', () => { - viewDocument.selection.setTo( selection2 ); - viewDocument.focus(); + view.change( writer => { + writer.setSelection( ViewRange.createFromParentsAndOffsets( text2, 0, text2, 3 ) ); + } ); + + view.focus(); } ); -viewDocument.render(); diff --git a/tests/view/manual/focusobserver.js b/tests/view/manual/focusobserver.js index e979a8d9f..acc032ef4 100644 --- a/tests/view/manual/focusobserver.js +++ b/tests/view/manual/focusobserver.js @@ -5,10 +5,12 @@ /* globals console, document */ -import Document from '../../../src/view/document'; -import { setData } from '../../../src/dev-utils/view'; +import View from '../../../src/view/view'; +import Position from '../../../src/view/position'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); +const view = new View(); +const viewDocument = view.document; viewDocument.on( 'focus', ( evt, data ) => console.log( `Focus in ${ data.domTarget.id }.` ) ); viewDocument.on( 'blur', ( evt, data ) => console.log( `Blur in ${ data.domTarget.id }.` ) ); @@ -16,22 +18,31 @@ viewDocument.on( 'blur', ( evt, data ) => console.log( `Blur in ${ data.domTarge const domEditable1 = document.getElementById( 'editable1' ); const domEditable2 = document.getElementById( 'editable2' ); -const editable1 = viewDocument.createRoot( domEditable1, 'editable1' ); -const editable2 = viewDocument.createRoot( domEditable2, 'editable2' ); +const editable1 = createViewRoot( viewDocument, 'div', 'editable1' ); +const editable2 = createViewRoot( viewDocument, 'div', 'editable2' ); + +view.attachDomRoot( domEditable1, 'editable1' ); +view.attachDomRoot( domEditable2, 'editable2' ); viewDocument.on( 'selectionChange', ( evt, data ) => { - viewDocument.selection.setTo( data.newSelection ); - viewDocument.render(); + view.change( writer => { + writer.setSelection( data.newSelection ); + } ); } ); -setData( viewDocument, '{}First editable.', { rootName: 'editable1' } ); -setData( viewDocument, 'Second editable.', { rootName: 'editable2' } ); +view.change( writer => { + writer.insert( Position.createAt( editable1 ), writer.createText( 'First editable.' ) ); + writer.insert( Position.createAt( editable2 ), writer.createText( 'Second editable.' ) ); + + writer.setSelection( editable1 ); +} ); editable1.on( 'change:isFocused', () => { domEditable1.style.backgroundColor = editable1.isFocused ? 'green' : 'red'; } ); + editable2.on( 'change:isFocused', () => { domEditable2.style.backgroundColor = editable2.isFocused ? 'green' : 'red'; } ); -viewDocument.focus(); +view.focus(); diff --git a/tests/view/manual/immutable.js b/tests/view/manual/immutable.js index 742f5c0cf..f6f0c969c 100644 --- a/tests/view/manual/immutable.js +++ b/tests/view/manual/immutable.js @@ -5,16 +5,32 @@ /* globals document */ -import Document from '../../../src/view/document'; -import { setData } from '../../../src/dev-utils/view'; +import View from '../../../src/view/view'; +import Position from '../../../src/view/position'; +import { parse } from '../../../src/dev-utils/view'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); -viewDocument.createRoot( document.getElementById( 'editor' ) ); +const view = new View(); +const viewDocument = view.document; +const viewRoot = createViewRoot( viewDocument, 'div' ); +view.attachDomRoot( document.getElementById( 'editor' ) ); -setData( viewDocument, - 'foo[]bar' + - '' + - '' + - 'bom' ); +view.change( writer => { + const { selection, view: data } = parse( + 'foo[]bar' + + '' + + '' + + 'bom' + ); -viewDocument.render(); + writer.insert( Position.createAt( viewRoot ), data ); + writer.setSelection( selection ); +} ); + +viewDocument.on( 'selectionChange', () => { + // Re-render view selection each time selection is changed. + // See https://github.com/ckeditor/ckeditor5-engine/issues/796. + view.render(); +} ); + +view.focus(); diff --git a/tests/view/manual/inline-filler.js b/tests/view/manual/inline-filler.js index 4d4673ea2..77b8b78dd 100644 --- a/tests/view/manual/inline-filler.js +++ b/tests/view/manual/inline-filler.js @@ -5,22 +5,30 @@ /* globals document */ -import Document from '../../../src/view/document'; -import { setData } from '../../../src/dev-utils/view'; - -const viewDocument = new Document(); -viewDocument.createRoot( document.getElementById( 'editor' ) ); - -viewDocument.isFocused = true; +import View from '../../../src/view/view'; +import Position from '../../../src/view/position'; +import createViewRoot from '../_utils/createroot'; +import { parse } from '../../../src/dev-utils/view'; + +const view = new View(); +const viewDocument = view.document; +const viewRoot = createViewRoot( viewDocument ); +view.attachDomRoot( document.getElementById( 'editor' ) ); + +view.change( writer => { + const { selection, view: data } = parse( + 'foo[]bar' + ); + + writer.insert( Position.createAt( viewRoot ), data ); + writer.setSelection( selection ); +} ); -setData( viewDocument, - 'foo[]bar' ); +view.focus(); viewDocument.on( 'selectionChange', ( evt, data ) => { - viewDocument.selection.setTo( data.newSelection ); - - // Needed due to https://github.com/ckeditor/ckeditor5-engine/issues/796. - viewDocument.render(); + view.change( writer => { + writer.setSelection( data.newSelection ); + } ); } ); -viewDocument.render(); diff --git a/tests/view/manual/keyobserver.js b/tests/view/manual/keyobserver.js index 28836eaec..a2d7706c0 100644 --- a/tests/view/manual/keyobserver.js +++ b/tests/view/manual/keyobserver.js @@ -5,15 +5,24 @@ /* globals console, document */ -import Document from '../../../src/view/document'; -import { setData } from '../../../src/dev-utils/view'; +import View from '../../../src/view/view'; +import Position from '../../../src/view/position'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); +const view = new View(); +const viewDocument = view.document; viewDocument.on( 'keydown', ( evt, data ) => console.log( 'keydown', data ) ); viewDocument.on( 'keyup', ( evt, data ) => console.log( 'keyup', data ) ); -viewDocument.createRoot( document.getElementById( 'editable' ), 'editable' ); -setData( viewDocument, 'foo{}bar', { rootName: 'editable' } ); -viewDocument.focus(); +const viewRoot = createViewRoot( viewDocument, 'div', 'editable' ); +view.attachDomRoot( document.getElementById( 'editable' ), 'editable' ); + +view.change( writer => { + const text = writer.createText( 'foobar' ); + writer.insert( Position.createAt( viewRoot ), text ); + writer.setSelection( text, 3 ); +} ); + +view.focus(); diff --git a/tests/view/manual/mutationobserver.js b/tests/view/manual/mutationobserver.js index 9a9578b72..f322258ae 100644 --- a/tests/view/manual/mutationobserver.js +++ b/tests/view/manual/mutationobserver.js @@ -5,19 +5,26 @@ /* globals console, document */ -import Document from '../../../src/view/document'; -import { setData } from '../../../src/dev-utils/view'; +import View from '../../../src/view/view'; +import Position from '../../../src/view/position'; +import createViewRoot from '../_utils/createroot'; +import { parse } from '../../../src/dev-utils/view'; -const viewDocument = new Document(); -viewDocument.createRoot( document.getElementById( 'editor' ) ); +const view = new View(); +const viewDocument = view.document; +const viewRoot = createViewRoot( viewDocument ); +view.attachDomRoot( document.getElementById( 'editor' ) ); viewDocument.on( 'mutations', ( evt, mutations ) => console.log( mutations ) ); viewDocument.on( 'selectionChange', ( evt, data ) => { - viewDocument.selection.setTo( data.newSelection ); + view.change( writer => writer.setSelection( data.newSelection ) ); } ); -setData( viewDocument, - 'foo' + - 'bar' ); +view.change( writer => { + const data = parse( + 'foo' + + 'bar' + ); -viewDocument.render(); + writer.insert( Position.createAt( viewRoot ), data ); +} ); diff --git a/tests/view/manual/noselection.js b/tests/view/manual/noselection.js index 4064ed7ec..1641e19fd 100644 --- a/tests/view/manual/noselection.js +++ b/tests/view/manual/noselection.js @@ -5,14 +5,21 @@ /* globals document */ -import Document from '../../../src/view/document'; +import View from '../../../src/view/view'; import { setData } from '../../../src/dev-utils/view'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); -viewDocument.createRoot( document.getElementById( 'editor' ) ); +const view = new View(); +const viewDocument = view.document; +createViewRoot( viewDocument ); +view.attachDomRoot( document.getElementById( 'editor' ) ); -setData( viewDocument, +viewDocument.on( 'selectionChange', () => { + // Re-render view selection each time selection is changed. + // See https://github.com/ckeditor/ckeditor5-engine/issues/796. + view.render(); +} ); + +setData( view, 'foo' + 'bar' ); - -viewDocument.render(); diff --git a/tests/view/manual/selectionobserver.js b/tests/view/manual/selectionobserver.js index 0ebd3219c..c301b8f2c 100644 --- a/tests/view/manual/selectionobserver.js +++ b/tests/view/manual/selectionobserver.js @@ -5,24 +5,25 @@ /* globals console, document */ -import Document from '../../../src/view/document'; +import View from '../../../src/view/view'; import { setData } from '../../../src/dev-utils/view'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); -viewDocument.createRoot( document.getElementById( 'editor' ) ); +const view = new View(); +const viewDocument = view.document; +createViewRoot( viewDocument ); +view.attachDomRoot( document.getElementById( 'editor' ) ); -setData( viewDocument, +setData( view, 'foobar' + 'bom' ); viewDocument.on( 'selectionChange', ( evt, data ) => { console.log( 'selectionChange', data ); - viewDocument.selection.setTo( data.newSelection ); + view.change( writer => writer.setSelection( data.newSelection ) ); } ); viewDocument.on( 'selectionChangeDone', ( evt, data ) => { console.log( '%c selectionChangeDone ', 'background: #222; color: #bada55', data ); - viewDocument.selection.setTo( data.newSelection ); + view.change( writer => writer.setSelection( data.newSelection ) ); } ); - -viewDocument.render(); diff --git a/tests/view/manual/uielement.js b/tests/view/manual/uielement.js index f6368831a..9cd2b1b3f 100644 --- a/tests/view/manual/uielement.js +++ b/tests/view/manual/uielement.js @@ -13,12 +13,10 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; -import UIElement from '../../../src/view/uielement'; import Position from '../../../src/view/position'; -import writer from '../../../src/view/writer'; -function createEndingUIElement() { - const element = new UIElement( 'span' ); +function createEndingUIElement( writer ) { + const element = writer.createUIElement( 'span' ); element.render = function( domDocument ) { const root = this.toDomElement( domDocument ); @@ -31,8 +29,8 @@ function createEndingUIElement() { return element; } -function createMiddleUIElement() { - const element = new UIElement( 'span' ); +function createMiddleUIElement( writer ) { + const element = writer.createUIElement( 'span' ); element.render = function( domDocument ) { const root = this.toDomElement( domDocument ); @@ -53,7 +51,7 @@ class UIElementTestPlugin extends Plugin { // Add some UIElement to each paragraph. editing.modelToView.on( 'insert:paragraph', ( evt, data, consumable, conversionApi ) => { const viewP = conversionApi.mapper.toViewElement( data.item ); - viewP.appendChildren( createEndingUIElement() ); + viewP.appendChildren( createEndingUIElement( conversionApi.writer ) ); }, { priority: 'lowest' } ); } } @@ -65,18 +63,19 @@ ClassicEditor } ) .then( editor => { window.editor = editor; + const view = editor.editing.view; // Add some UI elements. - const viewRoot = editor.editing.view.getRoot(); + const viewRoot = editor.editing.view.document.getRoot(); const viewText1 = viewRoot.getChild( 0 ).getChild( 0 ); const viewText2 = viewRoot.getChild( 1 ).getChild( 0 ); - writer.insert( new Position( viewText1, 20 ), createMiddleUIElement() ); - writer.insert( new Position( viewText1, 20 ), createMiddleUIElement() ); - writer.insert( new Position( viewText2, 0 ), createMiddleUIElement() ); - writer.insert( new Position( viewText2, 6 ), createMiddleUIElement() ); - - editor.editing.view.render(); + view.change( writer => { + writer.insert( new Position( viewText1, 20 ), createMiddleUIElement( writer ) ); + writer.insert( new Position( viewText1, 20 ), createMiddleUIElement( writer ) ); + writer.insert( new Position( viewText2, 0 ), createMiddleUIElement( writer ) ); + writer.insert( new Position( viewText2, 6 ), createMiddleUIElement( writer ) ); + } ); } ) .catch( err => { console.error( err.stack ); diff --git a/tests/view/manual/x-index.js b/tests/view/manual/x-index.js index 36f7933a6..76f48f5b9 100644 --- a/tests/view/manual/x-index.js +++ b/tests/view/manual/x-index.js @@ -5,24 +5,26 @@ /* globals console, document */ -import Document from '../../../src/view/document'; +import View from '../../../src/view/view'; import { setData } from '../../../src/dev-utils/view'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); -viewDocument.createRoot( document.getElementById( 'editor' ) ); +const view = new View(); +const viewDocument = view.document; +createViewRoot( viewDocument ); +view.attachDomRoot( document.getElementById( 'editor' ) ); -viewDocument.isFocused = true; - -setData( viewDocument, +setData( view, 'fo{}o' + '' + '' + 'bar' ); +view.focus(); + viewDocument.on( 'selectionChange', ( evt, data ) => { const node = data.newSelection.getFirstPosition().parent; console.log( node.name ? node.name : node._data ); - viewDocument.selection.setTo( data.newSelection ); -} ); -viewDocument.render(); + view.change( writer => writer.setSelection( data.newSelection ) ); +} ); From 44580b50545771a2007e248a4f36f092a84b80d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 5 Feb 2018 18:34:42 +0100 Subject: [PATCH 41/89] Using setData in manual tests. --- tests/view/manual/immutable.js | 22 ++++++++-------------- tests/view/manual/inline-filler.js | 19 ++++++++----------- tests/view/manual/mutationobserver.js | 17 ++++++----------- 3 files changed, 22 insertions(+), 36 deletions(-) diff --git a/tests/view/manual/immutable.js b/tests/view/manual/immutable.js index f6f0c969c..1abaab1c0 100644 --- a/tests/view/manual/immutable.js +++ b/tests/view/manual/immutable.js @@ -6,26 +6,20 @@ /* globals document */ import View from '../../../src/view/view'; -import Position from '../../../src/view/position'; -import { parse } from '../../../src/dev-utils/view'; +import { setData } from '../../../src/dev-utils/view'; import createViewRoot from '../_utils/createroot'; const view = new View(); const viewDocument = view.document; -const viewRoot = createViewRoot( viewDocument, 'div' ); +createViewRoot( viewDocument, 'div' ); view.attachDomRoot( document.getElementById( 'editor' ) ); -view.change( writer => { - const { selection, view: data } = parse( - 'foo[]bar' + - '' + - '' + - 'bom' - ); - - writer.insert( Position.createAt( viewRoot ), data ); - writer.setSelection( selection ); -} ); +setData( view, + 'foo[]bar' + + '' + + '' + + 'bom' +); viewDocument.on( 'selectionChange', () => { // Re-render view selection each time selection is changed. diff --git a/tests/view/manual/inline-filler.js b/tests/view/manual/inline-filler.js index 77b8b78dd..8978b221a 100644 --- a/tests/view/manual/inline-filler.js +++ b/tests/view/manual/inline-filler.js @@ -6,28 +6,25 @@ /* globals document */ import View from '../../../src/view/view'; -import Position from '../../../src/view/position'; import createViewRoot from '../_utils/createroot'; -import { parse } from '../../../src/dev-utils/view'; +import { setData } from '../../../src/dev-utils/view'; const view = new View(); const viewDocument = view.document; -const viewRoot = createViewRoot( viewDocument ); +createViewRoot( viewDocument ); view.attachDomRoot( document.getElementById( 'editor' ) ); -view.change( writer => { - const { selection, view: data } = parse( - 'foo[]bar' - ); - - writer.insert( Position.createAt( viewRoot ), data ); - writer.setSelection( selection ); -} ); +setData( + view, + 'foo[]bar' +); view.focus(); viewDocument.on( 'selectionChange', ( evt, data ) => { view.change( writer => { + // Re-render view selection each time selection is changed. + // See https://github.com/ckeditor/ckeditor5-engine/issues/796. writer.setSelection( data.newSelection ); } ); } ); diff --git a/tests/view/manual/mutationobserver.js b/tests/view/manual/mutationobserver.js index f322258ae..0ea7e1134 100644 --- a/tests/view/manual/mutationobserver.js +++ b/tests/view/manual/mutationobserver.js @@ -6,13 +6,12 @@ /* globals console, document */ import View from '../../../src/view/view'; -import Position from '../../../src/view/position'; import createViewRoot from '../_utils/createroot'; -import { parse } from '../../../src/dev-utils/view'; +import { setData } from '../../../src/dev-utils/view'; const view = new View(); const viewDocument = view.document; -const viewRoot = createViewRoot( viewDocument ); +createViewRoot( viewDocument ); view.attachDomRoot( document.getElementById( 'editor' ) ); viewDocument.on( 'mutations', ( evt, mutations ) => console.log( mutations ) ); @@ -20,11 +19,7 @@ viewDocument.on( 'selectionChange', ( evt, data ) => { view.change( writer => writer.setSelection( data.newSelection ) ); } ); -view.change( writer => { - const data = parse( - 'foo' + - 'bar' - ); - - writer.insert( Position.createAt( viewRoot ), data ); -} ); +setData( view, + 'foo' + + 'bar' +); From 80d346a0db9f6679288df826b04a3bf2f2f1eb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 5 Feb 2018 18:56:07 +0100 Subject: [PATCH 42/89] Using correct initialization on iframe manual test. --- tests/view/manual/noselection-iframe.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/view/manual/noselection-iframe.js b/tests/view/manual/noselection-iframe.js index d5d636077..d9321e9d4 100644 --- a/tests/view/manual/noselection-iframe.js +++ b/tests/view/manual/noselection-iframe.js @@ -5,15 +5,16 @@ /* globals document */ -import Document from '../../../src/view/document'; +import View from '../../../src/view/view'; import { setData } from '../../../src/dev-utils/view'; +import createViewRoot from '../_utils/createroot'; -const viewDocument = new Document(); +const view = new View(); +const viewDocument = view.document; +createViewRoot( viewDocument ); const iframe = document.getElementById( 'iframe' ); -viewDocument.createRoot( iframe.contentWindow.document.getElementById( 'editor' ) ); +view.attachDomRoot( iframe.contentWindow.document.getElementById( 'editor' ) ); -setData( viewDocument, +setData( view, 'foo' + 'bar' ); - -viewDocument.render(); From cbe9a51ba81e016601f8f2f4d83ce9772c339551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 6 Feb 2018 08:54:34 +0100 Subject: [PATCH 43/89] Changed EditableElement test to check if selection changes affects its focus. --- tests/view/editableelement.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/tests/view/editableelement.js b/tests/view/editableelement.js index bb6013446..1b0e59b97 100644 --- a/tests/view/editableelement.js +++ b/tests/view/editableelement.js @@ -75,7 +75,7 @@ describe( 'EditableElement', () => { expect( isFocusedSpy.calledOnce ).to.be.true; } ); - it( 'should change isFocused on document render event', () => { + it( 'should change isFocused when selection changes', () => { const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ); const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 ); docMock.selection._setTo( rangeMain ); @@ -85,34 +85,11 @@ describe( 'EditableElement', () => { expect( viewHeader.isFocused ).to.be.false; docMock.selection._setTo( [ rangeHeader ] ); - docMock.fire( 'render' ); expect( viewMain.isFocused ).to.be.false; expect( viewHeader.isFocused ).to.be.true; } ); - it( 'should change isFocus before actual rendering', done => { - const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ); - const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 ); - docMock.render = sinon.spy(); - - docMock.selection._setTo( rangeMain ); - docMock.isFocused = true; - - expect( viewMain.isFocused ).to.be.true; - expect( viewHeader.isFocused ).to.be.false; - - docMock.selection._setTo( [ rangeHeader ] ); - - viewHeader.on( 'change:isFocused', ( evt, propertyName, value ) => { - expect( value ).to.be.true; - sinon.assert.notCalled( docMock.render ); - done(); - } ); - - docMock.fire( 'render' ); - } ); - it( 'should change isFocused when document.isFocus changes', () => { const rangeMain = Range.createFromParentsAndOffsets( viewMain, 0, viewMain, 0 ); const rangeHeader = Range.createFromParentsAndOffsets( viewHeader, 0, viewHeader, 0 ); From ae4b9a9a9f3e127697e889f417f9194cfe789af3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 6 Feb 2018 11:44:29 +0100 Subject: [PATCH 44/89] Added docs and tests to engine view controller. --- src/view/view.js | 49 ++++++++++++++++++++-- tests/view/view/view.js | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/src/view/view.js b/src/view/view.js index f0faf5426..7f564707c 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -106,7 +106,20 @@ export default class View { injectQuirksHandling( this ); injectUiElementHandling( this ); + /** + * Is set to `true` when {@link #change view changes} are currently in progress. + * + * @private + * @member {Boolean} module:engine/view/view~View#_ongoingChange + */ this._ongoingChange = false; + + /** + * Is set to `true` when rendering view to DOM is currently in progress. + * + * @private + * @member {Boolean} module:engine/view/view~View#_renderingInProgress + */ this._renderingInProgress = false; } @@ -228,8 +241,8 @@ export default class View { } /** - * Focuses document. It will focus {@link module:engine/view/editableelement~EditableElement EditableElement} that is currently having - * selection inside. + * It will focus DOM element representing {@link module:engine/view/editableelement~EditableElement EditableElement} + * that is currently having selection inside. */ focus() { if ( !this.document.isFocused ) { @@ -251,6 +264,29 @@ export default class View { } } + /** + * Change method is the primary way of changing the view. You should use it to modify any node in the view tree. + * It makes sure that after all changes are made view is rendered to DOM. It prevents situations when DOM is updated + * when view state is not yet correct. It allows to nest calls one inside another and still perform single rendering + * after all changes are applied. + * + * view.change( writer => { + * writer.insert( position1, writer.createText( 'foo' ); + * + * view.change( writer => { + * writer.insert( position2, writer.createText( 'bar' ); + * } ); + * + * writer.remove( range ); + * } ); + * + * Change block is executed immediately. + * + * When the outermost block is done and rendering to DOM is over it fires {@link module:engine/view/document~Document#change } + * event. + * + * @param {Function} callback Callback function which may modify the view. + */ change( callback ) { if ( this._renderingInProgress ) { /** @@ -260,7 +296,7 @@ export default class View { */ log.warn( 'applying-view-changes-on-rendering: ' + - 'Attempting to make changes in the view during rendering process.' + + 'Attempting to make changes in the view during rendering process. ' + 'This may cause some unexpected behaviour and inconsistency between the DOM and the view.' ); } @@ -281,6 +317,10 @@ export default class View { } } + /** + * Renders {@link module:engine/view/document~Document view document} to DOM. If any view changes are + * currently in progress, rendering will start after all {@link #change change blocks} are processed. + */ 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. @@ -289,6 +329,9 @@ export default class View { } } + /** + * Destroys this instance. Makes sure that all observers are destroyed and listeners removed. + */ destroy() { for ( const observer of this._observers.values() ) { observer.destroy(); diff --git a/tests/view/view/view.js b/tests/view/view/view.js index c15fd3599..ae29e1b73 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -15,6 +15,7 @@ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import ViewRange from '../../../src/view/range'; import RootEditableElement from '../../../src/view/rooteditableelement'; import ViewElement from '../../../src/view/element'; +import ViewPosition from '../../../src/view/position'; import { isBlockFiller, BR_FILLER } from '../../../src/view/filler'; import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; @@ -91,6 +92,7 @@ describe( 'view', () => { expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv ); expect( view.renderer.markedChildren.has( viewRoot ) ).to.be.true; + domDiv.remove(); } ); it( 'should attach DOM element to custom view element', () => { @@ -394,6 +396,7 @@ describe( 'view', () => { expect( isBlockFiller( domDiv.childNodes[ 0 ], BR_FILLER ) ).to.be.true; view.destroy(); + domDiv.remove(); } ); it( 'should render changes in the Document', () => { @@ -436,6 +439,93 @@ describe( 'view', () => { expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'bar' ); view.destroy(); + domRoot.remove(); + } ); + + describe( 'change()', () => { + it( 'should call render and fire event after the change', () => { + const renderSpy = sinon.spy(); + const changeSpy = sinon.spy(); + view.renderer.on( 'render', renderSpy ); + view.document.on( 'change', changeSpy ); + + view.change( () => {} ); + + sinon.assert.callOrder( renderSpy, changeSpy ); + } ); + + it( 'should render and fire change event once for nested change blocks', () => { + const renderSpy = sinon.spy(); + const changeSpy = sinon.spy(); + view.renderer.on( 'render', renderSpy ); + view.document.on( 'change', changeSpy ); + + view.change( () => { + view.change( () => {} ); + view.change( () => { + view.change( () => {} ); + view.change( () => {} ); + } ); + view.change( () => {} ); + } ); + + sinon.assert.calledOnce( renderSpy ); + sinon.assert.calledOnce( changeSpy ); + sinon.assert.callOrder( renderSpy, changeSpy ); + } ); + + it( 'should render and fire change event once even if render is called during the change', () => { + const renderSpy = sinon.spy(); + const changeSpy = sinon.spy(); + view.renderer.on( 'render', renderSpy ); + view.document.on( 'change', changeSpy ); + + view.change( () => { + view.render(); + view.change( () => { + view.render(); + } ); + view.render(); + } ); + + sinon.assert.calledOnce( renderSpy ); + sinon.assert.calledOnce( changeSpy ); + sinon.assert.callOrder( renderSpy, changeSpy ); + } ); + + it( 'should log warning when someone tries to change view during rendering', () => { + const domDiv = document.createElement( 'div' ); + const viewRoot = createViewRoot( viewDocument, 'div', 'main' ); + sinon.stub( log, 'warn' ); + view.attachDomRoot( domDiv ); + + view.change( writer => { + const p = writer.createContainerElement( 'p' ); + const ui = writer.createUIElement( 'span' ); + + // This UIElement will try to modify view tree during rendering. + ui.render = function( domDocument ) { + const element = this.toDomElement( domDocument ); + + view.change( () => {} ); + + return element; + }; + + writer.insert( ViewPosition.createAt( p ), ui ); + writer.insert( ViewPosition.createAt( viewRoot ), p ); + } ); + + sinon.assert.calledOnce( log.warn ); + sinon.assert.calledWithExactly( log.warn, + 'applying-view-changes-on-rendering: ' + + 'Attempting to make changes in the view during rendering process. ' + + 'This may cause some unexpected behaviour and inconsistency between the DOM and the view.' + ); + + domDiv.remove(); + log.warn.restore(); + } ); } ); } ); From b1f8d2166590e2e8a5be85ee0704bef2b16388b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 6 Feb 2018 15:32:21 +0100 Subject: [PATCH 45/89] Fixed docs. --- src/conversion/buildmodelconverter.js | 2 +- src/model/utils/insertcontent.js | 2 +- src/view/attributeelement.js | 2 +- src/view/containerelement.js | 6 +-- src/view/editableelement.js | 4 +- src/view/observer/domeventdata.js | 2 +- src/view/observer/mutationobserver.js | 4 +- src/view/selection.js | 70 +++++++++++++-------------- src/view/view.js | 62 ++++++++++++++---------- src/view/writer.js | 2 +- 10 files changed, 84 insertions(+), 72 deletions(-) diff --git a/src/conversion/buildmodelconverter.js b/src/conversion/buildmodelconverter.js index 5da4864f8..35b6e26e9 100644 --- a/src/conversion/buildmodelconverter.js +++ b/src/conversion/buildmodelconverter.js @@ -178,7 +178,7 @@ class ModelConverterBuilder { * Default priority is `10`. * * **Note:** Keep in mind that event priority, that is set by this modifier, is used for attribute priority - * when {@link module:engine/view/writer~writer} is used. This changes how view elements are ordered, + * when {@link module:engine/view/writer~Writer} is used. This changes how view elements are ordered, * i.e.: `foo` vs `foo`. Using priority you can also * prevent node merging, i.e.: `foo` vs `foo`. * If you want to prevent merging, just set different priority for both converters. diff --git a/src/model/utils/insertcontent.js b/src/model/utils/insertcontent.js index 333876e5e..31bf332f3 100644 --- a/src/model/utils/insertcontent.js +++ b/src/model/utils/insertcontent.js @@ -165,9 +165,9 @@ class Insertion { } /** - * @private * Handles insertion of a single node. * + * @private * @param {module:engine/model/node~Node} node * @param {Object} context * @param {Boolean} context.isFirst Whether the given node is the first one in the content to be inserted. diff --git a/src/view/attributeelement.js b/src/view/attributeelement.js index f71783bb4..8e610d3de 100644 --- a/src/view/attributeelement.js +++ b/src/view/attributeelement.js @@ -14,7 +14,7 @@ const DEFAULT_PRIORITY = 10; /** * Attributes are elements which define document presentation. They are mostly elements like `` or ``. - * Attributes can be broken and merged by the {@link module:engine/view/writer~writer view writer}. + * Attributes can be broken and merged by the {@link module:engine/view/writer~Writer view writer}. * * Editing engine does not define fixed HTML DTD. This is why the type of the {@link module:engine/view/element~Element} need to * be defined by the feature developer. Creating an element you should use {@link module:engine/view/containerelement~ContainerElement} diff --git a/src/view/containerelement.js b/src/view/containerelement.js index c8c6a3289..b6928fe28 100644 --- a/src/view/containerelement.js +++ b/src/view/containerelement.js @@ -23,8 +23,8 @@ import Element from './element'; * DOM properly. {@link module:engine/view/domconverter~DomConverter} will ensure that `ContainerElement` is editable and it is possible * to put caret inside it, even if the container is empty. * - * Secondly, {@link module:engine/view/writer~writer view writer} uses this information. - * Nodes {@link module:engine/view/writer~writer.breakAttributes breaking} and {@link module:engine/view/writer~writer.mergeAttributes + * Secondly, {@link module:engine/view/writer~Writer view writer} uses this information. + * Nodes {@link module:engine/view/writer~Writer#breakAttributes breaking} and {@link module:engine/view/writer~Writer#mergeAttributes * merging} * is performed only in a bounds of a container nodes. * @@ -32,7 +32,7 @@ import Element from './element'; * *

fo^o

* - * {@link module:engine/view/writer~writer.breakAttributes breakAttributes} will create: + * {@link module:engine/view/writer~Writer#breakAttributes breakAttributes} will create: * *

foo

* diff --git a/src/view/editableelement.js b/src/view/editableelement.js index 0fab0aebc..7bdb694eb 100644 --- a/src/view/editableelement.js +++ b/src/view/editableelement.js @@ -41,8 +41,8 @@ export default class EditableElement extends ContainerElement { /** * Whether the editable is focused. * - * This property updates when {@link module:engine/view/document~Document#isFocused document.isFocused} is changed and after each - * {@link module:engine/view/document~Document#render render} method call. + * This property updates when {@link module:engine/view/document~Document#isFocused document.isFocused} or view + * selection is changed. * * @readonly * @observable diff --git a/src/view/observer/domeventdata.js b/src/view/observer/domeventdata.js index 1460eca81..f524330f6 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 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/mutationobserver.js b/src/view/observer/mutationobserver.js index f733a228f..f01e0b17d 100644 --- a/src/view/observer/mutationobserver.js +++ b/src/view/observer/mutationobserver.js @@ -48,14 +48,14 @@ export default class MutationObserver extends Observer { }; /** - * Reference to the {@link module:engine/view/document~Document#domConverter}. + * Reference to the {@link module:engine/view/view~View#domConverter}. * * @member {module:engine/view/domconverter~DomConverter} */ this.domConverter = view.domConverter; /** - * Reference to the {@link module:engine/view/document~Document#renderer}. + * Reference to the {@link module:engine/view/view~View#renderer}. * * @member {module:engine/view/renderer~Renderer} */ diff --git a/src/view/selection.js b/src/view/selection.js index 6984545e4..11eb78929 100644 --- a/src/view/selection.js +++ b/src/view/selection.js @@ -21,7 +21,7 @@ import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable'; * Class representing selection in tree view. * * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using - * {@link module:engine/view/selection~Selection#setTo} method. + * {@link module:engine/view/selection~Selection#_setTo} method. * That method create copies of provided ranges and store those copies internally. Further modifications to passed * ranges will not change selection's state. * Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges}, @@ -101,27 +101,6 @@ export default class Selection { } } - /** - * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection - * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to - * the user and selection over elements can be represented in other way, for example by applying proper CSS class. - * - * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be - * properly handled by screen readers). - * - * @protected - * @fires change - * @param {Boolean} [value=true] If set to true selection will be marked as `fake`. - * @param {Object} [options] Additional options. - * @param {String} [options.label=''] Fake selection label. - */ - _setFake( value = true, options = {} ) { - this._isFake = value; - this._fakeSelectionLabel = value ? options.label || '' : ''; - - this.fire( 'change' ); - } - /** * Returns true if selection instance is marked as `fake`. * @@ -390,6 +369,25 @@ export default class Selection { return true; } + /** + * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only + * one range in the selection, and that range contains exactly one element. + * Returns `null` if there is no selected element. + * + * @returns {module:engine/view/element~Element|null} + */ + getSelectedElement() { + if ( this.rangeCount !== 1 ) { + return null; + } + + const range = this.getFirstRange(); + const nodeAfterStart = range.start.nodeAfter; + const nodeBeforeEnd = range.end.nodeBefore; + + return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null; + } + /** * Removes all ranges that were added to the selection. * @@ -529,22 +527,24 @@ export default class Selection { } /** - * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only - * one range in the selection, and that range contains exactly one element. - * Returns `null` if there is no selected element. + * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection + * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to + * the user and selection over elements can be represented in other way, for example by applying proper CSS class. * - * @returns {module:engine/view/element~Element|null} + * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be + * properly handled by screen readers). + * + * @protected + * @fires change + * @param {Boolean} [value=true] If set to true selection will be marked as `fake`. + * @param {Object} [options] Additional options. + * @param {String} [options.label=''] Fake selection label. */ - getSelectedElement() { - if ( this.rangeCount !== 1 ) { - return null; - } - - const range = this.getFirstRange(); - const nodeAfterStart = range.start.nodeAfter; - const nodeBeforeEnd = range.end.nodeBefore; + _setFake( value = true, options = {} ) { + this._isFake = value; + this._fakeSelectionLabel = value ? options.label || '' : ''; - return ( nodeAfterStart instanceof Element && nodeAfterStart == nodeBeforeEnd ) ? nodeAfterStart : null; + this.fire( 'change' ); } /** diff --git a/src/view/view.js b/src/view/view.js index 7f564707c..b71b6f587 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -49,21 +49,17 @@ import { injectQuirksHandling } from './filler'; */ export default class View { constructor() { + /** + * Instance of the {@link module:engine/view/document~Document} associated with this view controller. + * + * @readonly + * @member {module:engine/view/document~Document} module:engine/view/view~View#document + */ this.document = new Document(); - this._writer = new Writer( this.document ); - - // TODO: check docs - // 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 - // 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 - * {@link module:engine/view/document~Document#renderer renderer} + * {@link module:engine/view/view~View#renderer renderer} * and {@link module:engine/view/observer/observer~Observer observers}. * * @readonly @@ -72,7 +68,7 @@ export default class View { this.domConverter = new DomConverter(); /** - * Instance of the {@link module:engine/view/document~Document#renderer renderer}. + * Instance of the {@link module:engine/view/renderer~Renderer renderer}. * * @readonly * @member {module:engine/view/renderer~Renderer} module:engine/view/view~View#renderer @@ -96,16 +92,6 @@ export default class View { */ 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 ); - /** * Is set to `true` when {@link #change view changes} are currently in progress. * @@ -121,6 +107,25 @@ export default class View { * @member {Boolean} module:engine/view/view~View#_renderingInProgress */ this._renderingInProgress = false; + + /** + * Writer instance used in {@link #change change method) callbacks. + * + * @private + * @member {module:engine/view/writer~Writer} module:engine/view/view~View#_writer + */ + this._writer = new Writer( this.document ); + + // Add default observers. + this.addObserver( MutationObserver ); + this.addObserver( SelectionObserver ); + this.addObserver( FocusObserver ); + this.addObserver( KeyObserver ); + this.addObserver( FakeSelectionObserver ); + + // Inject quirks handlers. + injectQuirksHandling( this ); + injectUiElementHandling( this ); } /** @@ -167,7 +172,7 @@ export default class View { /** * 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}. + * {@link #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 @@ -282,8 +287,8 @@ export default class View { * * Change block is executed immediately. * - * When the outermost block is done and rendering to DOM is over it fires {@link module:engine/view/document~Document#change } - * event. + * When the outermost change block is done and rendering to DOM is over it fires + * {@link module:engine/view/document~Document#event:change} event. * * @param {Function} callback Callback function which may modify the view. */ @@ -355,6 +360,13 @@ export default class View { this._renderingInProgress = false; } + + /** + * Fired after a topmost {@link module:engine/view/view~View#change change block} is finished and DOM rendering has + * been executed. + * + * @event module:engine/view/document~Document#event:change + */ } mix( View, ObservableMixin ); diff --git a/src/view/writer.js b/src/view/writer.js index 95c0fb7a9..9bbf4689e 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -68,7 +68,7 @@ export default class Writer { } /** - * Moves {@link module:engine/view/selection~Selection selection's} {@link #focus} to the specified location. + * Moves {@link module:engine/view/selection~Selection#focus selection's focus} to the specified location. * * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters. * From 2fd9058d376b2e55c26230fab653c620126a4b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 6 Feb 2018 18:19:38 +0100 Subject: [PATCH 46/89] Removed empty file. --- tests/conversion/advanced-converters.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/conversion/advanced-converters.js diff --git a/tests/conversion/advanced-converters.js b/tests/conversion/advanced-converters.js deleted file mode 100644 index e69de29bb..000000000 From cacae9fadaef289f3ac3b3adb78404dc4297d778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 7 Feb 2018 11:26:03 +0100 Subject: [PATCH 47/89] Updated documentation. --- src/view/observer/domeventdata.js | 8 +++++++- src/view/observer/observer.js | 6 ++++++ src/view/renderer.js | 2 -- src/view/view.js | 3 ++- tests/view/renderer.js | 10 ++++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/view/observer/domeventdata.js b/src/view/observer/domeventdata.js index f524330f6..00466c066 100644 --- a/src/view/observer/domeventdata.js +++ b/src/view/observer/domeventdata.js @@ -21,13 +21,19 @@ export default class DomEventData { * @param {Object} [additionalData] Additional properties that the instance should contain. */ constructor( view, domEvent, additionalData ) { + /** + * Instance of the view controller. + * + * @readonly + * @member {module:engine/view/view~View} module:engine/view/observer/observer~Observer.DomEvent#view + */ this.view = view; /** * The instance of the document. * * @readonly - * @member {module:engine/view/document~Document} module:engine/view/observer/observer~Observer.DomEvent#view + * @member {module:engine/view/document~Document} module:engine/view/observer/observer~Observer.DomEvent#document */ this.document = view.document; diff --git a/src/view/observer/observer.js b/src/view/observer/observer.js index a47ece19c..d0030da3c 100644 --- a/src/view/observer/observer.js +++ b/src/view/observer/observer.js @@ -24,6 +24,12 @@ export default class Observer { * @param {module:engine/view/view~View} view */ constructor( view ) { + /** + * Instance of the view controller. + * + * @readonly + * @member {module:engine/view/view~View} + */ this.view = view; /** diff --git a/src/view/renderer.js b/src/view/renderer.js index 319d65aaa..2073d3b17 100644 --- a/src/view/renderer.js +++ b/src/view/renderer.js @@ -110,8 +110,6 @@ export default class Renderer { * @type {null|HTMLElement} */ this._fakeSelectionContainer = null; - - // TODO: document render event. this.decorate( 'render' ); } diff --git a/src/view/view.js b/src/view/view.js index b71b6f587..87663916f 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -295,7 +295,8 @@ export default class View { change( callback ) { if ( this._renderingInProgress ) { /** - * TODO: description - there might be a view change triggered during rendering process. + * Warning displayed when there is an attempt to make changes in the view tree during the rendering process. + * This may cause unexpected behaviour and inconsistency between the DOM and the view. * * @error applying-view-changes-on-rendering */ diff --git a/tests/view/renderer.js b/tests/view/renderer.js index f3ac45324..7d642b3d2 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -140,6 +140,16 @@ describe( 'Renderer', () => { domRoot.remove(); } ); + it( 'should be decorated', () => { + const spy = sinon.spy(); + + renderer.on( 'render', spy ); + + renderer.render(); + + expect( spy.calledOnce ).to.be.true; + } ); + it( 'should update attributes', () => { viewRoot.setAttribute( 'class', 'foo' ); From deac6a1135ce0d213bfad43e284f565e7faf70cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 7 Feb 2018 12:58:00 +0100 Subject: [PATCH 48/89] Improved view writer's code coverage. --- src/view/writer.js | 13 +--- tests/view/writer/wrapposition.js | 0 tests/view/writer/writer.js | 113 ++++++++++++++++++++++++++++-- 3 files changed, 110 insertions(+), 16 deletions(-) delete mode 100644 tests/view/writer/wrapposition.js diff --git a/src/view/writer.js b/src/view/writer.js index 9bbf4689e..33cc4f039 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -58,7 +58,7 @@ export default class Writer { * * // Removes all ranges. * writer.setSelection( null ); - + * * @param {module:engine/view/selection~Selection|module:engine/view/position~Position| * Iterable.|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable * @param {Boolean|Number|'before'|'end'|'after'} [backwardSelectionOrOffset] @@ -123,17 +123,6 @@ export default class Writer { 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}. * diff --git a/tests/view/writer/wrapposition.js b/tests/view/writer/wrapposition.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/view/writer/writer.js b/tests/view/writer/writer.js index e598434a1..61faa6048 100644 --- a/tests/view/writer/writer.js +++ b/tests/view/writer/writer.js @@ -5,21 +5,126 @@ import Writer from '../../../src/view/writer'; import Document from '../../../src/view/document'; -import Text from '../../../src/view/text'; +import EditableElement from '../../../src/view/editableelement'; +import ViewPosition from '../../../src/view/position'; +import createViewRoot from '../_utils/createroot'; describe( 'Writer', () => { - let writer; + let writer, attributes, root; before( () => { - writer = new Writer( new Document() ); + attributes = { foo: 'bar', baz: 'quz' }; + const document = new Document(); + root = createViewRoot( document ); + writer = new Writer( document ); + } ); + + describe( 'setSelection()', () => { + it( 'should use selection._setTo method internally', () => { + const spy = sinon.spy( writer.document.selection, '_setTo' ); + const position = ViewPosition.createAt( root ); + writer.setSelection( position, true ); + + sinon.assert.calledWithExactly( spy, position, true ); + spy.restore(); + } ); + } ); + + describe( 'setSelectionFocus()', () => { + it( 'should use selection._setFocus method internally', () => { + const spy = sinon.spy( writer.document.selection, '_setFocus' ); + writer.setSelectionFocus( root, 0 ); + + sinon.assert.calledWithExactly( spy, root, 0 ); + spy.restore(); + } ); + } ); + + describe( 'setFakeSelection()', () => { + it( 'should use selection._setFake method internally', () => { + const spy = sinon.spy( writer.document.selection, '_setFake' ); + const options = {}; + writer.setFakeSelection( true, options ); + + sinon.assert.calledWithExactly( spy, true, options ); + spy.restore(); + } ); } ); describe( 'createText()', () => { it( 'should create Text instance', () => { const text = writer.createText( 'foo bar' ); - expect( text ).to.be.instanceOf( Text ); + expect( text.is( 'text' ) ).to.be.true; expect( text.data ).to.equal( 'foo bar' ); } ); } ); + + describe( 'createElement()', () => { + it( 'should create Element', () => { + const element = writer.createElement( 'foo', attributes ); + + expect( element.is( 'element' ) ).to.be.true; + expect( element.name ).to.equal( 'foo' ); + assertElementAttributes( element, attributes ); + } ); + } ); + + describe( 'createAttributeElement()', () => { + it( 'should create AttributeElement', () => { + const element = writer.createAttributeElement( 'foo', attributes ); + + expect( element.is( 'attributeElement' ) ).to.be.true; + expect( element.name ).to.equal( 'foo' ); + assertElementAttributes( element, attributes ); + } ); + } ); + + describe( 'createContainerElement()', () => { + it( 'should create ContainerElement', () => { + const element = writer.createContainerElement( 'foo', attributes ); + + expect( element.is( 'containerElement' ) ).to.be.true; + expect( element.name ).to.equal( 'foo' ); + assertElementAttributes( element, attributes ); + } ); + } ); + + describe( 'createEditableElement()', () => { + it( 'should create EditableElement', () => { + const element = writer.createEditableElement( 'foo', attributes ); + + expect( element ).to.be.instanceOf( EditableElement ); + expect( element.name ).to.equal( 'foo' ); + assertElementAttributes( element, attributes ); + } ); + } ); + + describe( 'createEmptyElement()', () => { + it( 'should create EmptyElement', () => { + const element = writer.createEmptyElement( 'foo', attributes ); + + expect( element.is( 'emptyElement' ) ).to.be.true; + expect( element.name ).to.equal( 'foo' ); + assertElementAttributes( element, attributes ); + } ); + } ); + + describe( 'createUIElement()', () => { + it( 'should create UIElement', () => { + const element = writer.createUIElement( 'foo', attributes ); + + expect( element.is( 'uiElement' ) ).to.be.true; + expect( element.name ).to.equal( 'foo' ); + assertElementAttributes( element, attributes ); + } ); + } ); + + function assertElementAttributes( element, attributes ) { + for ( const key of Object.keys( attributes ) ) { + if ( element.getAttribute( key ) !== attributes[ key ] ) { + throw new Error( 'Attributes in element are different that those passed to the constructor method.' ); + } + } + } } ); From 3f9b87ab88c71a9ea1d3f229cc3ed7126c3ba1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 7 Feb 2018 13:17:16 +0100 Subject: [PATCH 49/89] Removed default params from view writer.setFakeSelection() method. --- src/view/writer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/writer.js b/src/view/writer.js index 33cc4f039..0157195ea 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -93,7 +93,7 @@ export default class Writer { * @param {Object} [options] Additional options. * @param {String} [options.label=''] Fake selection label. */ - setFakeSelection( value = true, options = {} ) { + setFakeSelection( value, options ) { this.document.selection._setFake( value, options ); } From b962a486cbbf66ee175d4ff3a4ded016999cadbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Fri, 9 Feb 2018 09:04:23 +0100 Subject: [PATCH 50/89] Small docs fixes. --- src/controller/editingcontroller.js | 2 +- src/view/view.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index 4491b11b7..25e004e76 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -49,7 +49,7 @@ export default class EditingController { this.model = model; /** - * Editing view. + * Editing view controller. * * @readonly * @member {module:engine/view/view~View} diff --git a/src/view/view.js b/src/view/view.js index 87663916f..08828af18 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -318,7 +318,6 @@ export default class View { this._ongoingChange = false; - // TODO: docs for the event. this.document.fire( 'change' ); } } From 199be4ab31609cc229d95b9d121b90331be2c318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Fri, 9 Feb 2018 16:13:57 +0100 Subject: [PATCH 51/89] More fixes to tests after changes in the view. --- tests/conversion/downcast-converters.js | 4 ++-- tests/conversion/two-way-converters.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index f517b90b5..c4ee57cec 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -39,9 +39,9 @@ describe( 'downcast-helpers', () => { // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. - controller.view.getRoot()._name = 'div'; + controller.view.document.getRoot()._name = 'div'; - viewRoot = controller.view.getRoot(); + viewRoot = controller.view.document.getRoot(); conversion = new Conversion(); conversion.register( 'downcast', [ controller.downcastDispatcher ] ); diff --git a/tests/conversion/two-way-converters.js b/tests/conversion/two-way-converters.js index e63b0130c..135f2462c 100644 --- a/tests/conversion/two-way-converters.js +++ b/tests/conversion/two-way-converters.js @@ -30,7 +30,7 @@ describe( 'two-way-converters', () => { const modelDoc = model.document; modelRoot = modelDoc.createRoot(); - viewRoot = controller.view.getRoot(); + viewRoot = controller.view.document.getRoot(); // Set name of view root the same as dom root. // This is a mock of attaching view root to dom root. viewRoot._name = 'div'; From 181f113231c39fbc95a8981e74615187ed447a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Sun, 11 Feb 2018 22:52:22 +0100 Subject: [PATCH 52/89] Updated downcast converters to use view writer where possible. --- src/conversion/downcast-converters.js | 103 ++-- src/conversion/model-to-view-converters.js | 622 --------------------- tests/conversion/downcast-converters.js | 73 +-- 3 files changed, 66 insertions(+), 732 deletions(-) delete mode 100644 src/conversion/model-to-view-converters.js diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index f0eab1234..9af3c06a7 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -31,8 +31,6 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep'; * * downcastElementToElement( { model: 'paragraph', view: 'p' }, 'high' ); * - * downcastElementToElement( { model: 'paragraph', view: new ViewContainerElement( 'p' ) } ); - * * downcastElementToElement( { * model: 'fancyParagraph', * view: { @@ -43,17 +41,23 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep'; * * downcastElementToElement( { * model: 'heading', - * view: modelElement => new ViewContainerElement( 'h' + modelElement.getAttribute( 'level' ) ) + * view: ( modelItem, consumable, conversionApi ) => { + * const viewWriter = conversionApi.writer; + * + * return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ); + * } * } ); * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. * * @param {Object} config Conversion configuration. * @param {String} config.model Name of the model element to convert. - * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function| - * module:engine/view/containerelement~ContainerElement} config.view View element name, or a view element definition, - * or a function that takes model element as a parameter and returns a view container element, - * or a view container element instance. The view element will be used then in conversion. + * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name, or a + * view element definition, or a function that will be provided with all the parameters of the dispatcher's + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}. + * It's expected that the function returns a {@link module:engine/view/containerelement~ContainerElement}. + * The view element will be used then in conversion. + * * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority. * @returns {Function} Conversion helper. */ @@ -77,8 +81,6 @@ export function downcastElementToElement( config, priority = 'normal' ) { * * downcastAttributeToElement( 'bold', { view: 'strong' }, 'high' ); * - * downcastAttributeToElement( 'bold', { view: new ViewAttributeElement( 'strong' ) } ); - * * downcastAttributeToElement( 'bold', { * view: { * name: 'span', @@ -116,7 +118,11 @@ export function downcastElementToElement( config, priority = 'normal' ) { * ] ); * * downcastAttributeToElement( 'bold', { - * view: modelAttributeValue => new ViewAttributeElement( 'span', { style: 'font-weight:' + modelAttributeValue } ) + * view: ( modelAttributeValue, data, consumable, conversionApi ) => { + * const viewWriter = conversionApi.writer; + * + * return viewWriter( 'span', { style: 'font-weight:' + modelAttributeValue } ); + * } * } ); * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. @@ -125,9 +131,8 @@ export function downcastElementToElement( config, priority = 'normal' ) { * @param {Object|Array.} config Conversion configuration. It is possible to provide multiple configurations in an array. * @param {*} [config.model] The value of the converted model attribute for which the `view` property is defined. * If omitted, the configuration item will be used as a "default" configuration when no other item matches the attribute value. - * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function| - * module:engine/view/attributeelement~AttributeElement} config.view View element name, or a view element definition, - * or a function that takes model element as a parameter and returns a view attribute element, or a view attribute element instance. + * @param {String|module:engine/view/elementdefinition~ElementDefinition|Function} config.view View element name, + * or a view element definition, or a function that takes model element as a parameter and returns a view attribute element. * The view element will be used then in conversion. * @param {module:utils/priorities~PriorityString} [priority='normal'] Converter priority. * @returns {Function} Conversion helper. @@ -246,14 +251,14 @@ export function downcastAttributeToAttribute( modelAttributeKey, config = {}, pr * * downcastMarkerToElement( { * model: 'search', - * view: data => { - * return new ViewUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } ); + * view: ( data, conversionApi ) => { + * return conversionApi.writer.createUIElement( 'span', { 'data-marker': 'search', 'data-start': data.isOpening } ); * } * } ); * * If function is passed as `config.view` parameter, it will be used to generate both boundary elements. The function * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}. - * The `data` object properties are passed from + * The `data` and `conversionApi` objects are passed from * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. Additionally, * `data.isOpening` parameter is passed, which is set to `true` for marker start boundary element, and `false` to * marker end boundary element. @@ -317,8 +322,8 @@ export function downcastMarkerToElement( config, priority = 'normal' ) { * } ); * * If function is passed as `config.view` parameter, it will be used to generate highlight descriptor. The function - * receives `data` object as parameter and should return an instance of {@link module:engine/view/uielement~UIElement view.UIElement}. - * The `data` object properties are passed from + * receives `data` and `conversionApi` objects as parameters and should return + * {@link module:engine/conversion/downcast-converters~HighlightDescriptor}. The `data` and `conversionApi` objects are passed from * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:addMarker}. * * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process. @@ -353,17 +358,19 @@ function _normalizeToElementConfig( config, ViewElementClass ) { return; } + const view = config.view; + // Build `.view` property. - // It is expected to be either creator function or view element instance. - if ( typeof config.view == 'string' ) { - // If `.view` is a string, create a proper view element instance out of given `ViewElementClass` and name given in `.view`. - config.view = new ViewElementClass( config.view ); - } else if ( typeof config.view == 'object' && !( config.view instanceof ViewElementClass ) ) { + // It is expected to be either string, element definition or creator function. + if ( typeof view == 'string' ) { + // If `.view` is a string, create a function that returns view element instance out of given `ViewElementClass`. + config.view = () => new ViewElementClass( view ); + } else if ( typeof view == 'object' ) { // If `.view` is an object, use it to build view element instance. - config.view = _createViewElementFromDefinition( config.view, ViewElementClass ); + const element = _createViewElementFromDefinition( view, ViewElementClass ); + config.view = () => element.clone(); } - // `.view` can be also a function or already a view element instance. - // These are already valid types which don't have to be normalized. + // `.view` can be also a function that is already valid type which don't have to be normalized. } // Creates view element instance from provided viewElementDefinition and class. @@ -458,12 +465,8 @@ function _getCreatorForArrayConfig( config ) { // If there was default config or matched config... if ( matchedConfigEntry ) { - // If the entry `.view` is a function, execute it and return the value... - if ( typeof matchedConfigEntry.view == 'function' ) { - return matchedConfigEntry.view( modelAttributeValue ); - } - // Else, just return `.view`, it should be a view element instance after it got normalized earlier. - return matchedConfigEntry.view; + // The entry `.view` is a function after it got normalized earlier, execute it and return the value. + return matchedConfigEntry.view( modelAttributeValue ); } return null; @@ -472,21 +475,20 @@ function _getCreatorForArrayConfig( config ) { /** * Function factory, creates a converter that converts node insertion changes from the model to the view. - * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed, - * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the - * dispatcher's {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}. + * Passed function will be provided with all the parameters of the dispatcher's + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:insert insert event}. * It's expected that the function returns a {@link module:engine/view/element~Element}. * The result of the function will be inserted to the view. * * The converter automatically consumes corresponding value from consumables list, stops the event (see * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and bind model and view elements. * - * modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) ); - * * modelDispatcher.on( * 'insert:myElem', * insertElement( ( modelItem, consumable, conversionApi ) => { - * let myElem = new ViewElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, new ViewText( 'myText' ) ); + * const writer = conversionApi.writer; + * const text = writer.createText( 'myText' ); + * const myElem = writer.createElement( 'myElem', { myAttr: 'my-' + modelItem.getAttribute( 'myAttr' ) }, text ); * * // Do something fancy with myElem using `modelItem` or other parameters. * @@ -494,15 +496,12 @@ function _getCreatorForArrayConfig( config ) { * } * ) ); * - * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which - * will be inserted. + * @param {Function} elementCreator Function returning a view element, which will be inserted. * @returns {Function} Insert element event converter. */ export function insertElement( elementCreator ) { return ( evt, data, consumable, conversionApi ) => { - const viewElement = ( elementCreator instanceof ViewElement ) ? - elementCreator.clone( true ) : - elementCreator( data.item, consumable, conversionApi ); + const viewElement = elementCreator( data.item, consumable, conversionApi ); if ( !viewElement ) { return; @@ -751,8 +750,7 @@ export function changeAttribute( attributeCreator ) { * |- b {bold: true} | |- ab * |- c |- c * - * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and - * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the + * Passed `Function` will be provided with attribute value and then all the parameters of the * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher#event:attribute attribute event}. * It's expected that the function returns a {@link module:engine/view/element~Element}. * The result of the function will be the wrapping element. @@ -761,24 +759,21 @@ export function changeAttribute( attributeCreator ) { * The converter automatically consumes corresponding value from consumables list, stops the event (see * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}). * - * modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) ); + * modelDispatcher.on( 'attribute:bold', wrapItem( ( attributeValue, data, consumable, conversionApi ) => { + * return conversionApi.writer.createAttributeElement( 'strong' ); + * } ); * - * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will - * be used for wrapping. + * @param {Function} elementCreator Function returning a view element, which will be used for wrapping. * @returns {Function} Set/change attribute converter. */ export function wrap( elementCreator ) { return ( evt, data, consumable, conversionApi ) => { // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed // or the attribute was removed. - const oldViewElement = ( elementCreator instanceof ViewElement ) ? - elementCreator.clone( true ) : - elementCreator( data.attributeOldValue, data, consumable, conversionApi ); + const oldViewElement = elementCreator( data.attributeOldValue, data, consumable, conversionApi ); // Create node to wrap with. - const newViewElement = ( elementCreator instanceof ViewElement ) ? - elementCreator.clone( true ) : - elementCreator( data.attributeNewValue, data, consumable, conversionApi ); + const newViewElement = elementCreator( data.attributeNewValue, data, consumable, conversionApi ); if ( !oldViewElement && !newViewElement ) { return; diff --git a/src/conversion/model-to-view-converters.js b/src/conversion/model-to-view-converters.js deleted file mode 100644 index b2b4d57ec..000000000 --- a/src/conversion/model-to-view-converters.js +++ /dev/null @@ -1,622 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import ModelRange from '../model/range'; -import ModelSelection from '../model/selection'; -import ModelElement from '../model/element'; - -import ViewElement from '../view/element'; -import ViewAttributeElement from '../view/attributeelement'; -import ViewText from '../view/text'; -import ViewRange from '../view/range'; -import DocumentSelection from '../model/documentselection'; - -/** - * Contains model to view converters for - * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}. - * - * @module engine/conversion/model-to-view-converters - */ - -/** - * Function factory, creates a converter that converts node insertion changes from the model to the view. - * The view element that will be added to the view depends on passed parameter. If {@link module:engine/view/element~Element} was passed, - * it will be cloned and the copy will be inserted. If `Function` is provided, it is passed all the parameters of the - * dispatcher's {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:insert insert event}. - * It's expected that the function returns a {@link module:engine/view/element~Element}. - * The result of the function will be inserted to the view. - * - * The converter automatically consumes corresponding value from consumables list, stops the event (see - * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}) and bind model and view elements. - * - * modelDispatcher.on( 'insert:paragraph', insertElement( new ViewElement( 'p' ) ) ); - * - * modelDispatcher.on( - * 'insert:myElem', - * insertElement( ( data, consumable, conversionApi ) => { - * let myElem = new ViewElement( 'myElem', { myAttr: true }, new ViewText( 'myText' ) ); - * - * // Do something fancy with myElem using data/consumable/conversionApi ... - * - * return myElem; - * } - * ) ); - * - * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which - * will be inserted. - * @returns {Function} Insert element event converter. - */ -export function insertElement( elementCreator ) { - return ( evt, data, consumable, conversionApi ) => { - const viewElement = ( elementCreator instanceof ViewElement ) ? - elementCreator.clone( true ) : - elementCreator( data, consumable, conversionApi ); - - if ( !viewElement ) { - return; - } - - if ( !consumable.consume( data.item, 'insert' ) ) { - return; - } - - const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); - - conversionApi.mapper.bindElements( data.item, viewElement ); - conversionApi.writer.insert( viewPosition, viewElement ); - }; -} - -/** - * Function factory, creates a default model-to-view converter for text insertion changes. - * - * The converter automatically consumes corresponding value from consumables list and stops the event (see - * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}). - * - * modelDispatcher.on( 'insert:$text', insertText() ); - * - * @returns {Function} Insert text event converter. - */ -export function insertText() { - return ( evt, data, consumable, conversionApi ) => { - if ( !consumable.consume( data.item, 'insert' ) ) { - return; - } - - const viewPosition = conversionApi.mapper.toViewPosition( data.range.start ); - const viewText = new ViewText( data.item.data ); - - conversionApi.writer.insert( viewPosition, viewText ); - }; -} - -/** - * Function factory, creates a default model-to-view converter for node remove changes. - * - * modelDispatcher.on( 'remove', remove() ); - * - * @returns {Function} Remove event converter. - */ -export function remove() { - return ( evt, data, conversionApi ) => { - // Find view range start position by mapping model position at which the remove happened. - const viewStart = conversionApi.mapper.toViewPosition( data.position ); - - const modelEnd = data.position.getShiftedBy( data.length ); - const viewEnd = conversionApi.mapper.toViewPosition( modelEnd, { isPhantom: true } ); - - const viewRange = new ViewRange( viewStart, viewEnd ); - - // Trim the range to remove in case some UI elements are on the view range boundaries. - const removed = conversionApi.writer.remove( viewRange.getTrimmed() ); - - // After the range is removed, unbind all view elements from the model. - // Range inside view document fragment is used to unbind deeply. - for ( const child of ViewRange.createIn( removed ).getItems() ) { - conversionApi.mapper.unbindViewElement( child ); - } - }; -} - -/** - * Function factory, creates a converter that converts marker adding change to the view ui element. - * The view ui element that will be added to the view depends on passed parameter. See {@link ~insertElement}. - * In a case of collapsed range element will not wrap range but separate elements will be placed at the beginning - * and at the end of the range. - * - * **Note:** unlike {@link ~insertElement}, the converter does not bind view element to model, because this converter - * uses marker as "model source of data". This means that view ui element does not have corresponding model element. - * - * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning a view element, which - * will be inserted. - * @returns {Function} Insert element event converter. - */ -export function insertUIElement( elementCreator ) { - return ( evt, data, consumable, conversionApi ) => { - let viewStartElement, viewEndElement; - - // Create two view elements. One will be inserted at the beginning of marker, one at the end. - // If marker is collapsed, only "opening" element will be inserted. - if ( elementCreator instanceof ViewElement ) { - viewStartElement = elementCreator.clone( true ); - viewEndElement = elementCreator.clone( true ); - } else { - data.isOpening = true; - viewStartElement = elementCreator( data, conversionApi ); - - data.isOpening = false; - viewEndElement = elementCreator( data, conversionApi ); - } - - if ( !viewStartElement || !viewEndElement ) { - return; - } - - const markerRange = data.markerRange; - - // Marker that is collapsed has consumable build differently that non-collapsed one. - // For more information see `addMarker` event description. - // If marker's range is collapsed - check if it can be consumed. - if ( markerRange.isCollapsed && !consumable.consume( markerRange, evt.name ) ) { - return; - } - - // If marker's range is not collapsed - consume all items inside. - for ( const value of markerRange ) { - if ( !consumable.consume( value.item, evt.name ) ) { - return; - } - } - - const mapper = conversionApi.mapper; - const writer = conversionApi.writer; - - // Add "opening" element. - writer.insert( mapper.toViewPosition( markerRange.start ), viewStartElement ); - - // Add "closing" element only if range is not collapsed. - if ( !markerRange.isCollapsed ) { - writer.insert( mapper.toViewPosition( markerRange.end ), viewEndElement ); - } - - evt.stop(); - }; -} - -/** - * Function factory, creates a default model-to-view converter for removing {@link module:engine/view/uielement~UIElement ui element} - * basing on marker remove change. - * - * @param {module:engine/view/uielement~UIElement|Function} elementCreator View ui element, or function returning - * a view ui element, which will be used as a pattern when look for element to remove at the marker start position. - * @returns {Function} Remove ui element converter. - */ -export function removeUIElement( elementCreator ) { - return ( evt, data, conversionApi ) => { - let viewStartElement, viewEndElement; - - // Create two view elements. One will be used to remove "opening element", the other for "closing element". - // If marker was collapsed, only "opening" element will be removed. - if ( elementCreator instanceof ViewElement ) { - viewStartElement = elementCreator.clone( true ); - viewEndElement = elementCreator.clone( true ); - } else { - data.isOpening = true; - viewStartElement = elementCreator( data, conversionApi ); - - data.isOpening = false; - viewEndElement = elementCreator( data, conversionApi ); - } - - if ( !viewStartElement || !viewEndElement ) { - return; - } - - const markerRange = data.markerRange; - const writer = conversionApi.writer; - - // When removing the ui elements, we map the model range to view twice, because that view range - // may change after the first clearing. - if ( !markerRange.isCollapsed ) { - writer.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewEndElement ); - } - - // Remove "opening" element. - writer.clear( conversionApi.mapper.toViewRange( markerRange ).getEnlarged(), viewStartElement ); - - evt.stop(); - }; -} - -/** - * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view. - * - * Attributes from model are converted to the view element attributes in the view. You may provide a custom function to generate - * a key-value attribute pair to add/change/remove. If not provided, model attributes will be converted to view elements - * attributes on 1-to-1 basis. - * - * **Note:** Provided attribute creator should always return the same `key` for given attribute from the model. - * - * The converter automatically consumes corresponding value from consumables list and stops the event (see - * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}). - * - * modelDispatcher.on( 'attribute:customAttr:myElem', changeAttribute( ( data ) => { - * // Change attribute key from `customAttr` to `class` in view. - * const key = 'class'; - * let value = data.attributeNewValue; - * - * // Force attribute value to 'empty' if the model element is empty. - * if ( data.item.childCount === 0 ) { - * value = 'empty'; - * } - * - * // Return key-value pair. - * return { key, value }; - * } ) ); - * - * @param {Function} [attributeCreator] Function returning an object with two properties: `key` and `value`, which - * represents attribute key and attribute value to be set on a {@link module:engine/view/element~Element view element}. - * The function is passed all the parameters of the - * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute} event. - * @returns {Function} Set/change attribute converter. - */ -export function changeAttribute( attributeCreator ) { - attributeCreator = attributeCreator || ( ( value, key ) => ( { value, key } ) ); - - return ( evt, data, consumable, conversionApi ) => { - if ( !consumable.consume( data.item, evt.name ) ) { - return; - } - - const { key, value } = attributeCreator( data.attributeNewValue, data.attributeKey, data, consumable, conversionApi ); - - if ( data.attributeNewValue !== null ) { - conversionApi.mapper.toViewElement( data.item ).setAttribute( key, value ); - } else { - conversionApi.mapper.toViewElement( data.item ).removeAttribute( key ); - } - }; -} - -/** - * Function factory, creates a converter that converts set/change/remove attribute changes from the model to the view. - * Also can be used to convert selection attributes. In that case, an empty attribute element will be created and the - * selection will be put inside it. - * - * Attributes from model are converted to a view element that will be wrapping those view nodes that are bound to - * model elements having given attribute. This is useful for attributes like `bold`, which may be set on text nodes in model - * but are represented as an element in the view: - * - * [paragraph] MODEL ====> VIEW

- * |- a {bold: true} |- - * |- b {bold: true} | |- ab - * |- c |- c - * - * The wrapping node depends on passed parameter. If {@link module:engine/view/element~Element} was passed, it will be cloned and - * the copy will become the wrapping element. If `Function` is provided, it is passed attribute value and then all the parameters of the - * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher#event:attribute attribute event}. - * It's expected that the function returns a {@link module:engine/view/element~Element}. - * The result of the function will be the wrapping element. - * When provided `Function` does not return element, then will be no conversion. - * - * The converter automatically consumes corresponding value from consumables list, stops the event (see - * {@link module:engine/conversion/modelconversiondispatcher~ModelConversionDispatcher}). - * - * modelDispatcher.on( 'attribute:bold', wrapItem( new ViewAttributeElement( 'strong' ) ) ); - * - * @param {module:engine/view/element~Element|Function} elementCreator View element, or function returning a view element, which will - * be used for wrapping. - * @returns {Function} Set/change attribute converter. - */ -export function wrap( elementCreator ) { - return ( evt, data, consumable, conversionApi ) => { - // Recreate current wrapping node. It will be used to unwrap view range if the attribute value has changed - // or the attribute was removed. - const oldViewElement = ( elementCreator instanceof ViewElement ) ? - elementCreator.clone( true ) : - elementCreator( data.attributeOldValue, data, consumable, conversionApi ); - - // Create node to wrap with. - const newViewElement = ( elementCreator instanceof ViewElement ) ? - elementCreator.clone( true ) : - elementCreator( data.attributeNewValue, data, consumable, conversionApi ); - - if ( !oldViewElement && !newViewElement ) { - return; - } - - if ( !consumable.consume( data.item, evt.name ) ) { - return; - } - - const viewWriter = conversionApi.writer; - const viewSelection = viewWriter.document.selection; - - if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) { - // Selection attribute conversion. - viewWriter.wrap( viewSelection.getFirstRange(), newViewElement, viewSelection ); - } else { - // Node attribute conversion. - let viewRange = conversionApi.mapper.toViewRange( data.range ); - - // First, unwrap the range from current wrapper. - if ( data.attributeOldValue !== null ) { - viewRange = viewWriter.unwrap( viewRange, oldViewElement ); - } - - if ( data.attributeNewValue !== null ) { - viewWriter.wrap( viewRange, newViewElement ); - } - } - }; -} - -/** - * Function factory, creates converter that converts text inside marker's range. Converter wraps the text with - * {@link module:engine/view/attributeelement~AttributeElement} created from provided descriptor. - * See {link module:engine/conversion/model-to-view-converters~createViewElementFromHighlightDescriptor}. - * - * Also can be used to convert selection that is inside a marker. In that case, an empty attribute element will be - * created and the selection will be put inside it. - * - * If the highlight descriptor will not provide `priority` property, `10` will be used. - * - * If the highlight descriptor will not provide `id` property, name of the marker will be used. - * - * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor - * @return {Function} - */ -export function highlightText( highlightDescriptor ) { - return ( evt, data, consumable, conversionApi ) => { - if ( data.markerRange.isCollapsed ) { - return; - } - - if ( !( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) && !data.item.is( 'textProxy' ) ) { - return; - } - - const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi ); - - if ( !descriptor ) { - return; - } - - if ( !consumable.consume( data.item, evt.name ) ) { - return; - } - - const viewElement = createViewElementFromHighlightDescriptor( descriptor ); - const viewWriter = conversionApi.writer; - const viewSelection = viewWriter.document.selection; - - if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) { - viewWriter.wrap( viewSelection.getFirstRange(), viewElement, viewSelection ); - } else { - const viewRange = conversionApi.mapper.toViewRange( data.range ); - viewWriter.wrap( viewRange, viewElement ); - } - }; -} - -/** - * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range. - * - * The converter checks if an element has `addHighlight` function stored as - * {@link module:engine/view/element~Element#setCustomProperty custom property} and, if so, uses it to apply the highlight. - * In such case converter will consume all element's children, assuming that they were handled by element itself. - * - * When `addHighlight` custom property is not present, element is not converted in any special way. - * This means that converters will proceed to convert element's child nodes. - * - * If the highlight descriptor will not provide `priority` property, `10` will be used. - * - * If the highlight descriptor will not provide `id` property, name of the marker will be used. - * - * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor - * @return {Function} - */ -export function highlightElement( highlightDescriptor ) { - return ( evt, data, consumable, conversionApi ) => { - if ( data.markerRange.isCollapsed ) { - return; - } - - if ( !( data.item instanceof ModelElement ) ) { - return; - } - - const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi ); - - if ( !descriptor ) { - return; - } - - if ( !consumable.test( data.item, evt.name ) ) { - return; - } - - const viewElement = conversionApi.mapper.toViewElement( data.item ); - - if ( viewElement && viewElement.getCustomProperty( 'addHighlight' ) ) { - // Consume element itself. - consumable.consume( data.item, evt.name ); - - // Consume all children nodes. - for ( const value of ModelRange.createIn( data.item ) ) { - consumable.consume( value.item, evt.name ); - } - - viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor ); - } - }; -} - -/** - * Function factory, creates a converter that converts model marker remove to the view. - * - * Both text nodes and elements are handled by this converter by they are handled a bit differently. - * - * Text nodes are unwrapped using {@link module:engine/view/attributeelement~AttributeElement} created from provided - * highlight descriptor. See {link module:engine/conversion/model-to-view-converters~highlightDescriptorToAttributeElement}. - * - * For elements, the converter checks if an element has `removeHighlight` function stored as - * {@link module:engine/view/element~Element#setCustomProperty custom property}. If so, it uses it to remove the highlight. - * In such case, children of that element will not be converted. - * - * When `removeHighlight` is not present, element is not converted in any special way. - * Instead converter will proceed to convert element's child nodes. - * - * If the highlight descriptor will not provide `priority` property, `10` will be used. - * - * If the highlight descriptor will not provide `id` property, name of the marker will be used. - * - * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor|Function} highlightDescriptor - * @return {Function} - */ -export function removeHighlight( highlightDescriptor ) { - return ( evt, data, conversionApi ) => { - // This conversion makes sense only for non-collapsed range. - if ( data.markerRange.isCollapsed ) { - return; - } - - const descriptor = _prepareDescriptor( highlightDescriptor, data, conversionApi ); - - if ( !descriptor ) { - return; - } - - const viewRange = conversionApi.mapper.toViewRange( data.markerRange ); - - // Retrieve all items in the affected range. We will process them and remove highlight from them appropriately. - const items = new Set( viewRange.getItems() ); - - // First, iterate through all items and remove highlight from those container elements that have custom highlight handling. - for ( const item of items ) { - if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) { - item.getCustomProperty( 'removeHighlight' )( item, descriptor.id ); - - // If container element had custom handling, remove all it's children from further processing. - for ( const descendant of ViewRange.createIn( item ) ) { - items.delete( descendant ); - } - } - } - - // Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end - // to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged). - const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor ); - - for ( const item of Array.from( items ).reverse() ) { - if ( item.is( 'textProxy' ) ) { - conversionApi.writer.unwrap( ViewRange.createOn( item ), viewHighlightElement ); - } - } - }; -} - -// Helper function for `highlight`. Prepares the actual descriptor object using value passed to the converter. -function _prepareDescriptor( highlightDescriptor, data, conversionApi ) { - // If passed descriptor is a creator function, call it. If not, just use passed value. - const descriptor = typeof highlightDescriptor == 'function' ? - highlightDescriptor( data, conversionApi ) : - highlightDescriptor; - - if ( !descriptor ) { - return null; - } - - // Apply default descriptor priority. - if ( !descriptor.priority ) { - descriptor.priority = 10; - } - - // Default descriptor id is marker name. - if ( !descriptor.id ) { - descriptor.id = data.markerName; - } - - return descriptor; -} - -/** - * Creates `span` {@link module:engine/view/attributeelement~AttributeElement view attribute element} from information - * provided by {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. If priority - * is not provided in descriptor - default priority will be used. - * - * @param {module:engine/conversion/model-to-view-converters~HighlightDescriptor} descriptor - * @return {module:engine/conversion/model-to-view-converters~HighlightAttributeElement} - */ -export function createViewElementFromHighlightDescriptor( descriptor ) { - const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes ); - - if ( descriptor.class ) { - const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ]; - viewElement.addClass( ...cssClasses ); - } - - if ( descriptor.priority ) { - viewElement.priority = descriptor.priority; - } - - viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id ); - - return viewElement; -} - -/** - * Special kind of {@link module:engine/view/attributeelement~AttributeElement} that is created and used in - * marker-to-highlight conversion. - * - * The difference between `HighlightAttributeElement` and {@link module:engine/view/attributeelement~AttributeElement} - * is {@link module:engine/view/attributeelement~AttributeElement#isSimilar} method. - * - * For `HighlightAttributeElement` it checks just `highlightDescriptorId` custom property, that is set during marker-to-highlight - * conversion basing on {@link module:engine/conversion/model-to-view-converters~HighlightDescriptor} object. - * `HighlightAttributeElement`s with same `highlightDescriptorId` property are considered similar. - */ -class HighlightAttributeElement extends ViewAttributeElement { - isSimilar( otherElement ) { - if ( otherElement.is( 'attributeElement' ) ) { - return this.getCustomProperty( 'highlightDescriptorId' ) === otherElement.getCustomProperty( 'highlightDescriptorId' ); - } - - return false; - } -} - -/** - * Object describing how the content highlight should be created in the view. - * - * Each text node contained in the highlight will be wrapped with `span` element with CSS class(es), attributes and priority - * described by this object. - * - * Each element can handle displaying the highlight separately by providing `addHighlight` and `removeHighlight` custom - * properties: - * * `HighlightDescriptor` is passed to the `addHighlight` function upon conversion and should be used to apply the highlight to - * the element, - * * descriptor id is passed to the `removeHighlight` function upon conversion and should be used to remove the highlight of given - * id from the element. - * - * @typedef {Object} module:engine/conversion/model-to-view-converters~HighlightDescriptor - * - * @property {String|Array.} class CSS class or array of classes to set. If descriptor is used to - * create {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those classes will be set - * on that {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, - * usually those class will be set on that element, however this depends on how the element converts the descriptor. - * - * @property {String} [id] Descriptor identifier. If not provided, defaults to converted marker's name. - * - * @property {Number} [priority] Descriptor priority. If not provided, defaults to `10`. If descriptor is used to create - * {@link module:engine/view/attributeelement~AttributeElement}, it will be that element's - * {@link module:engine/view/attributeelement~AttributeElement#priority}. If descriptor is applied to an element, - * the priority will be used to determine which descriptor is more important. - * - * @property {Object} [attributes] Attributes to set. If descriptor is used to create - * {@link module:engine/view/attributeelement~AttributeElement} over text nodes, those attributes will be set on that - * {@link module:engine/view/attributeelement~AttributeElement}. If descriptor is applied to an element, usually those - * attributes will be set on that element, however this depends on how the element converts the descriptor. - */ diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index c4ee57cec..23aa680f7 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -73,21 +73,6 @@ describe( 'downcast-helpers', () => { expectResult( '' ); } ); - it( 'config.view is an element instance', () => { - const helper = downcastElementToElement( { - model: 'paragraph', - view: new ViewContainerElement( 'p' ) - } ); - - conversion.for( 'downcast' ).add( helper ); - - model.change( writer => { - writer.insertElement( 'paragraph', modelRoot, 0 ); - } ); - - expectResult( '

' ); - } ); - it( 'config.view is a view element definition', () => { const helper = downcastElementToElement( { model: 'fancyParagraph', @@ -148,20 +133,6 @@ describe( 'downcast-helpers', () => { expectResult( 'foo' ); } ); - it( 'config.view is an element instance', () => { - const helper = downcastAttributeToElement( 'bold', { - view: new ViewAttributeElement( 'strong' ) - } ); - - conversion.for( 'downcast' ).add( helper ); - - model.change( writer => { - writer.insertText( 'foo', { bold: true }, modelRoot, 0 ); - } ); - - expectResult( 'foo' ); - } ); - it( 'config.view is a view element definition', () => { const helper = downcastAttributeToElement( 'bold', { view: { @@ -421,22 +392,6 @@ describe( 'downcast-helpers', () => { expectResult( 'foo' ); } ); - it( 'config.view is an element instance', () => { - const helper = downcastMarkerToElement( { - model: 'search', - view: new ViewUIElement( 'span', { 'data-marker': 'search' } ) - } ); - - conversion.for( 'downcast' ).add( helper ); - - model.change( writer => { - writer.insertText( 'foo', modelRoot, 0 ); - writer.setMarker( 'search', ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 2 ) ); - } ); - - expectResult( 'foo' ); - } ); - it( 'config.view is a view element definition', () => { const helper = downcastMarkerToElement( { model: 'search', @@ -550,7 +505,13 @@ describe( 'downcast-converters', () => { dispatcher = controller.downcastDispatcher; - dispatcher.on( 'insert:paragraph', insertElement( () => new ViewContainerElement( 'p' ) ) ); + dispatcher.on( + 'insert:paragraph', + insertElement( + ( modelItem, consumable, conversionApi ) => conversionApi.writer.createContainerElement( 'p' ) + ) + ); + dispatcher.on( 'attribute:class', changeAttribute() ); modelRootStart = ModelPosition.createAt( modelRoot, 0 ); @@ -685,7 +646,7 @@ describe( 'downcast-converters', () => { return { key: 'class', value }; }; - dispatcher.on( 'insert:div', insertElement( new ViewContainerElement( 'div' ) ) ); + dispatcher.on( 'insert:div', insertElement( ( model, consumable, api ) => api.writer.createContainerElement( 'div' ) ) ); dispatcher.on( 'attribute:theme', changeAttribute( themeConverter ) ); const modelParagraph = new ModelElement( 'paragraph', { theme: 'nice' }, new ModelText( 'foobar' ) ); @@ -729,9 +690,9 @@ describe( 'downcast-converters', () => { describe( 'wrap', () => { it( 'should convert insert/change/remove of attribute in model into wrapping element in a view', () => { const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); - const viewB = new ViewAttributeElement( 'b' ); + const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' ); - dispatcher.on( 'attribute:bold', wrap( viewB ) ); + dispatcher.on( 'attribute:bold', wrap( creator ) ); model.change( writer => { writer.insert( modelElement, modelRootStart ); @@ -749,9 +710,9 @@ describe( 'downcast-converters', () => { it( 'should convert insert/remove of attribute in model with wrapping element generating function as a parameter', () => { const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { style: 'bold' } ) ); - const elementGenerator = value => { + const elementGenerator = ( value, data, consumable, api ) => { if ( value == 'bold' ) { - return new ViewAttributeElement( 'b' ); + return api.writer.createAttributeElement( 'b' ); } }; @@ -777,7 +738,7 @@ describe( 'downcast-converters', () => { new ModelText( 'x' ) ] ); - const elementGenerator = href => new ViewAttributeElement( 'a', { href } ); + const elementGenerator = ( href, data, consumable, api ) => api.writer.createAttributeElement( 'a', { href } ); dispatcher.on( 'attribute:link', wrap( elementGenerator ) ); @@ -797,9 +758,9 @@ describe( 'downcast-converters', () => { it( 'should support unicode', () => { const modelElement = new ModelElement( 'paragraph', null, [ 'நி', new ModelText( 'லைக்', { bold: true } ), 'கு' ] ); - const viewB = new ViewAttributeElement( 'b' ); + const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' ); - dispatcher.on( 'attribute:bold', wrap( viewB ) ); + dispatcher.on( 'attribute:bold', wrap( creator ) ); model.change( writer => { writer.insert( modelElement, modelRootStart ); @@ -816,9 +777,9 @@ describe( 'downcast-converters', () => { it( 'should be possible to override wrap', () => { const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar', { bold: true } ) ); - const viewB = new ViewAttributeElement( 'b' ); + const creator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'b' ); - dispatcher.on( 'attribute:bold', wrap( viewB ) ); + dispatcher.on( 'attribute:bold', wrap( creator ) ); dispatcher.on( 'attribute:bold', ( evt, data, consumable ) => { consumable.consume( data.item, 'attribute:bold' ); }, { priority: 'high' } ); From fa87b53dcab6de536c67edc173493554ca56f200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 12 Feb 2018 00:00:44 +0100 Subject: [PATCH 53/89] More downcast conversion changes connected to view refactoring. --- tests/conversion/definition-based-converters.js | 0 tests/conversion/downcast-selection-converters.js | 9 +++++---- 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 tests/conversion/definition-based-converters.js diff --git a/tests/conversion/definition-based-converters.js b/tests/conversion/definition-based-converters.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index 5e0266dd1..d3caad96a 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -9,8 +9,6 @@ import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; import View from '../../src/view/view'; -import ViewContainerElement from '../../src/view/containerelement'; -import ViewAttributeElement from '../../src/view/attributeelement'; import ViewUIElement from '../../src/view/uielement'; import Mapper from '../../src/conversion/mapper'; @@ -59,7 +57,9 @@ describe( 'downcast-selection-converters', () => { dispatcher = new DowncastDispatcher( model, { mapper, viewSelection } ); dispatcher.on( 'insert:$text', insertText() ); - dispatcher.on( 'attribute:bold', wrap( new ViewAttributeElement( 'strong' ) ) ); + + const strongCreator = ( value, data, consumable, api ) => api.writer.createAttributeElement( 'strong' ); + dispatcher.on( 'attribute:bold', wrap( strongCreator ) ); dispatcher.on( 'addMarker:marker', highlightText( highlightDescriptor ) ); dispatcher.on( 'addMarker:marker', highlightElement( highlightDescriptor ) ); @@ -502,7 +502,8 @@ describe( 'downcast-selection-converters', () => { model.schema.extend( '$text', { allowIn: 'td' } ); // "Universal" converter to convert table structure. - const tableConverter = insertElement( modelItem => new ViewContainerElement( modelItem.name ) ); + const containerCreator = ( item, consumable, api ) => api.writer.createContainerElement( item.name ); + const tableConverter = insertElement( containerCreator ); dispatcher.on( 'insert:table', tableConverter ); dispatcher.on( 'insert:tr', tableConverter ); dispatcher.on( 'insert:td', tableConverter ); From b361fd6cecfe474012b88c9837b39f84cb3031ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Mon, 12 Feb 2018 16:25:34 +0100 Subject: [PATCH 54/89] Passing all conversion parameters to creator from array confing. --- src/conversion/downcast-converters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 9af3c06a7..66ed65115 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -450,7 +450,7 @@ function _getCreatorForArrayConfig( config ) { const defaultConfig = config.find( configEntry => configEntry.model === undefined ); // Return a creator function. - return modelAttributeValue => { + return ( modelAttributeValue, data, consumable, conversionApi ) => { // Set default config at first. It will be used if no other entry matches model attribute value. let matchedConfigEntry = defaultConfig; @@ -466,7 +466,7 @@ function _getCreatorForArrayConfig( config ) { // If there was default config or matched config... if ( matchedConfigEntry ) { // The entry `.view` is a function after it got normalized earlier, execute it and return the value. - return matchedConfigEntry.view( modelAttributeValue ); + return matchedConfigEntry.view( modelAttributeValue, data, consumable, conversionApi ); } return null; From 1e46743d17d1eb41383ddaba5dc328178789b914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 10:55:55 +0100 Subject: [PATCH 55/89] Fixed typos. --- src/conversion/downcast-converters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 66ed65115..a2c725ad4 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -44,7 +44,7 @@ import cloneDeep from '@ckeditor/ckeditor5-utils/src/lib/lodash/cloneDeep'; * view: ( modelItem, consumable, conversionApi ) => { * const viewWriter = conversionApi.writer; * - * return viewWriter.createContainerElement( 'h' + modelElement.getAttribute( 'level' ) ); + * return viewWriter.createContainerElement( 'h' + modelItem.getAttribute( 'level' ) ); * } * } ); * @@ -121,7 +121,7 @@ export function downcastElementToElement( config, priority = 'normal' ) { * view: ( modelAttributeValue, data, consumable, conversionApi ) => { * const viewWriter = conversionApi.writer; * - * return viewWriter( 'span', { style: 'font-weight:' + modelAttributeValue } ); + * return viewWriter( 'span', { style: 'font-weight:' + modelAttributeValue } ); * } * } ); * From 983ce9b7da06fd1531bfbc49e5870f22e721d8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 11:24:17 +0100 Subject: [PATCH 56/89] Unified usage of view.Writer in converters. --- src/conversion/downcast-converters.js | 5 +++-- src/conversion/downcast-selection-converters.js | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index a2c725ad4..71ade89f4 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -483,7 +483,7 @@ function _getCreatorForArrayConfig( config ) { * The converter automatically consumes corresponding value from consumables list, stops the event (see * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}) and bind model and view elements. * - * modelDispatcher.on( + * downcastDispatcher.on( * 'insert:myElem', * insertElement( ( modelItem, consumable, conversionApi ) => { * const writer = conversionApi.writer; @@ -961,10 +961,11 @@ export function removeHighlight( highlightDescriptor ) { // Then, iterate through all other items. Look for text nodes and unwrap them. Start from the end // to prevent errors when view structure changes when unwrapping (and, for example, some attributes are merged). const viewHighlightElement = createViewElementFromHighlightDescriptor( descriptor ); + const viewWriter = conversionApi.writer; for ( const item of Array.from( items ).reverse() ) { if ( item.is( 'textProxy' ) ) { - conversionApi.writer.unwrap( ViewRange.createOn( item ), viewHighlightElement ); + viewWriter.unwrap( ViewRange.createOn( item ), viewHighlightElement ); } } }; diff --git a/src/conversion/downcast-selection-converters.js b/src/conversion/downcast-selection-converters.js index 43949d6cc..cfbb0be1e 100644 --- a/src/conversion/downcast-selection-converters.js +++ b/src/conversion/downcast-selection-converters.js @@ -77,12 +77,12 @@ export function convertCollapsedSelection() { return; } - const writer = conversionApi.writer; + const viewWriter = conversionApi.writer; const modelPosition = selection.getFirstPosition(); const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); - const brokenPosition = writer.breakAttributes( viewPosition ); + const brokenPosition = viewWriter.breakAttributes( viewPosition ); - writer.setSelection( brokenPosition ); + viewWriter.setSelection( brokenPosition ); }; } @@ -112,8 +112,8 @@ export function convertCollapsedSelection() { */ export function clearAttributes() { return ( evt, data, consumable, conversionApi ) => { - const writer = conversionApi.writer; - const viewSelection = writer.document.selection; + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; for ( const range of viewSelection.getRanges() ) { // Not collapsed selection should not have artifacts. @@ -124,7 +124,7 @@ export function clearAttributes() { } } } - writer.setSelection( null ); + viewWriter.setSelection( null ); }; } From 75d1d07b1d745ef62ff79ece1f480e8204ef5460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 12:03:13 +0100 Subject: [PATCH 57/89] Fixed docs in view.Writer. --- src/view/writer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/writer.js b/src/view/writer.js index 1dfe1d92e..25a6158fe 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -608,7 +608,7 @@ export default class Writer { * Wraps elements within range with provided {@link module:engine/view/attributeelement~AttributeElement AttributeElement}. * If a collapsed range is provided, it will be wrapped only if it is equal to view selection. * - * If `viewSelection` was set and a collapsed range was passed, if the range is same as selection, the selection + * If a collapsed range was passed and is same as selection, the selection * will be moved to the inside of the wrapped attribute element. * * Throws {@link module:utils/ckeditorerror~CKEditorError} `view-writer-invalid-range-container` From 85ae40611a59a77615470d59b43e46012d0db80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 12:48:12 +0100 Subject: [PATCH 58/89] Removed unused view selection from writer.wrap(). call. --- src/conversion/downcast-converters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 71ade89f4..f27637aa8 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -788,7 +788,7 @@ export function wrap( elementCreator ) { if ( data.item instanceof ModelSelection || data.item instanceof DocumentSelection ) { // Selection attribute conversion. - viewWriter.wrap( viewSelection.getFirstRange(), newViewElement, viewSelection ); + viewWriter.wrap( viewSelection.getFirstRange(), newViewElement ); } else { // Node attribute conversion. let viewRange = conversionApi.mapper.toViewRange( data.range ); From b4e527327e86137b9be4a2ad7c2b790bae4492f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 15:15:16 +0100 Subject: [PATCH 59/89] Removed writer.createElement() method. --- src/view/writer.js | 15 --------------- tests/view/writer/writer.js | 10 ---------- 2 files changed, 25 deletions(-) diff --git a/src/view/writer.js b/src/view/writer.js index 25a6158fe..30a92157d 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -17,7 +17,6 @@ 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'; /** @@ -109,20 +108,6 @@ export default class Writer { 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/attributeelement~AttributeElement}. * diff --git a/tests/view/writer/writer.js b/tests/view/writer/writer.js index 61faa6048..14b19a98d 100644 --- a/tests/view/writer/writer.js +++ b/tests/view/writer/writer.js @@ -60,16 +60,6 @@ describe( 'Writer', () => { } ); } ); - describe( 'createElement()', () => { - it( 'should create Element', () => { - const element = writer.createElement( 'foo', attributes ); - - expect( element.is( 'element' ) ).to.be.true; - expect( element.name ).to.equal( 'foo' ); - assertElementAttributes( element, attributes ); - } ); - } ); - describe( 'createAttributeElement()', () => { it( 'should create AttributeElement', () => { const element = writer.createAttributeElement( 'foo', attributes ); From 3a546da211ac65d64a89cada3642527de630d9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 16:10:22 +0100 Subject: [PATCH 60/89] Using writer to set attribute on view element. --- src/conversion/downcast-converters.js | 7 +- src/dev-utils/view.js | 2 +- src/view/domconverter.js | 2 +- src/view/element.js | 55 +++++----- src/view/placeholder.js | 2 +- src/view/writer.js | 133 +++++++++++++----------- tests/conversion/downcast-converters.js | 4 +- tests/conversion/viewconsumable.js | 4 +- tests/view/domconverter/view-to-dom.js | 12 +-- tests/view/element.js | 42 ++++---- tests/view/node.js | 9 +- tests/view/renderer.js | 6 +- tests/view/view/view.js | 2 +- 13 files changed, 143 insertions(+), 137 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index f27637aa8..9dee0b586 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -722,16 +722,19 @@ export function changeAttribute( attributeCreator ) { // First remove the old attribute if there was one. const oldAttribute = attributeCreator( data.attributeOldValue, data, consumable, conversionApi ); + const mapper = conversionApi.mapper; + const viewWriter = conversionApi.writer; if ( data.attributeOldValue !== null && oldAttribute ) { - conversionApi.mapper.toViewElement( data.item ).removeAttribute( oldAttribute.key ); + mapper.toViewElement( data.item ).removeAttribute( oldAttribute.key ); } // Then, if conversion was successful, set the new attribute. const newAttribute = attributeCreator( data.attributeNewValue, data, consumable, conversionApi ); if ( data.attributeNewValue !== null && newAttribute ) { - conversionApi.mapper.toViewElement( data.item ).setAttribute( newAttribute.key, newAttribute.value ); + const viewElement = mapper.toViewElement( data.item ); + viewWriter.setAttribute( newAttribute.key, newAttribute.value, viewElement ); } }; } diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js index 388a07c4f..02d1019c5 100644 --- a/src/dev-utils/view.js +++ b/src/dev-utils/view.js @@ -930,7 +930,7 @@ function _convertElement( viewElement ) { // Move attributes. for ( const attributeKey of viewElement.getAttributeKeys() ) { - newElement.setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) ); + newElement._setAttribute( attributeKey, viewElement.getAttribute( attributeKey ) ); } return newElement; diff --git a/src/view/domconverter.js b/src/view/domconverter.js index 405eb0169..0d20baf50 100644 --- a/src/view/domconverter.js +++ b/src/view/domconverter.js @@ -413,7 +413,7 @@ export default class DomConverter { const attrs = domNode.attributes; for ( let i = attrs.length - 1; i >= 0; i-- ) { - viewElement.setAttribute( attrs[ i ].name, attrs[ i ].value ); + viewElement._setAttribute( attrs[ i ].name, attrs[ i ].value ); } } diff --git a/src/view/element.js b/src/view/element.js index 780ced1f2..c20cac5fc 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -317,27 +317,6 @@ export default class Element extends Node { return this._attrs.has( key ); } - /** - * Adds or overwrite attribute with a specified key and value. - * - * @param {String} key Attribute key. - * @param {String} value Attribute value. - * @fires module:engine/view/node~Node#change - */ - setAttribute( key, value ) { - value = String( value ); - - this._fireChange( 'attributes', this ); - - if ( key == 'class' ) { - parseClasses( this._classes, value ); - } else if ( key == 'style' ) { - parseInlineStyles( this._styles, value ); - } else { - this._attrs.set( key, value ); - } - } - /** * Inserts a child node or a list of child nodes on the given index and sets the parent of these nodes to * this element. @@ -686,12 +665,12 @@ export default class Element extends Node { * * For example: * - * const element = new ViewElement( 'foo' ); - * element.setAttribute( 'banana', '10' ); - * element.setAttribute( 'apple', '20' ); - * element.setStyle( 'color', 'red' ); - * element.setStyle( 'border-color', 'white' ); - * element.addClass( 'baz' ); + * const element = new ViewElement( 'foo', { + * banana: '10', + * apple: '20', + * style: 'color: red; border-color: white;', + * class: 'baz' + * } ); * * // returns 'foo class="baz" style="border-color:white;color:red" apple="20" banana="10"' * element.getIdentity(); @@ -711,6 +690,28 @@ export default class Element extends Node { ( attributes == '' ? '' : ` ${ attributes }` ); } + /** + * Adds or overwrite attribute with a specified key and value. + * + * @protected + * @param {String} key Attribute key. + * @param {String} value Attribute value. + * @fires module:engine/view/node~Node#change + */ + _setAttribute( key, value ) { + value = String( value ); + + this._fireChange( 'attributes', this ); + + if ( key == 'class' ) { + parseClasses( this._classes, value ); + } else if ( key == 'style' ) { + parseInlineStyles( this._styles, value ); + } else { + this._attrs.set( key, value ); + } + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/placeholder.js b/src/view/placeholder.js index dd34de0d3..a6b411a3d 100644 --- a/src/view/placeholder.js +++ b/src/view/placeholder.js @@ -42,7 +42,7 @@ export function attachPlaceholder( view, element, placeholderText, checkFunction // Store text in element's data attribute. // This data attribute is used in CSS class to show the placeholder. - element.setAttribute( 'data-placeholder', placeholderText ); + element._setAttribute( 'data-placeholder', placeholderText ); // Store information about placeholder. documentPlaceholders.get( document ).set( element, checkFunction ); diff --git a/src/view/writer.js b/src/view/writer.js index 30a92157d..f5c3ddc4e 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -182,6 +182,10 @@ export default class Writer { return new UIElement( name, attributes ); } + setAttribute( key, value, element ) { + element._setAttribute( key, value ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. @@ -878,7 +882,7 @@ export default class Writer { */ _wrapRange( range, attribute ) { // Range is inside single attribute and spans on all children. - if ( rangeSpansOnAllChildren( range ) && wrapAttributeElement( attribute, range.start.parent ) ) { + if ( rangeSpansOnAllChildren( range ) && this._wrapAttributeElement( attribute, range.start.parent ) ) { const parent = range.start.parent; const end = this.mergeAttributes( Position.createAfter( parent ) ); @@ -894,7 +898,7 @@ export default class Writer { if ( breakEnd.isEqual( breakStart.getShiftedBy( 1 ) ) ) { const node = breakStart.nodeAfter; - if ( node instanceof AttributeElement && wrapAttributeElement( attribute, node ) ) { + if ( node instanceof AttributeElement && this._wrapAttributeElement( attribute, node ) ) { const start = this.mergeAttributes( breakStart ); if ( !start.isEqual( breakStart ) ) { @@ -979,6 +983,70 @@ export default class Writer { // If position is next to text node - move position inside. return movePositionToTextNode( newPosition ); } + + /** + * Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by + * merging them if possible. When merging is possible - all attributes, styles and classes are moved from wrapper + * element to element being wrapped. + * + * @private + * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement. + * @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element. + * @returns {Boolean} Returns `true` if elements are merged. + */ + _wrapAttributeElement( wrapper, toWrap ) { + // Can't merge if name or priority differs. + if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) { + return false; + } + + // Check if attributes can be merged. + for ( const key of wrapper.getAttributeKeys() ) { + // Classes and styles should be checked separately. + if ( key === 'class' || key === 'style' ) { + continue; + } + + // If some attributes are different we cannot wrap. + if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) { + return false; + } + } + + // Check if styles can be merged. + for ( const key of wrapper.getStyleNames() ) { + if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) { + return false; + } + } + + // Move all attributes/classes/styles from wrapper to wrapped AttributeElement. + for ( const key of wrapper.getAttributeKeys() ) { + // Classes and styles should be checked separately. + if ( key === 'class' || key === 'style' ) { + continue; + } + + // Move only these attributes that are not present - other are similar. + if ( !toWrap.hasAttribute( key ) ) { + this.setAttribute( key, wrapper.getAttribute( key ), toWrap ); + } + } + + for ( const key of wrapper.getStyleNames() ) { + if ( !toWrap.hasStyle( key ) ) { + toWrap.setStyle( key, wrapper.getStyle( key ) ); + } + } + + for ( const key of wrapper.getClassNames() ) { + if ( !toWrap.hasClass( key ) ) { + toWrap.addClass( key ); + } + } + + return true; + } } // Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements. @@ -1230,67 +1298,6 @@ function mergeTextNodes( t1, t2 ) { return new Position( t1, nodeBeforeLength ); } -// Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by merging them if possible. -// When merging is possible - all attributes, styles and classes are moved from wrapper element to element being -// wrapped. -// -// @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement. -// @param {module:engine/view/attributeelement~AttributeElement} toWrap AttributeElement to wrap using wrapper element. -// @returns {Boolean} Returns `true` if elements are merged. -function wrapAttributeElement( wrapper, toWrap ) { - // Can't merge if name or priority differs. - if ( wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority ) { - return false; - } - - // Check if attributes can be merged. - for ( const key of wrapper.getAttributeKeys() ) { - // Classes and styles should be checked separately. - if ( key === 'class' || key === 'style' ) { - continue; - } - - // If some attributes are different we cannot wrap. - if ( toWrap.hasAttribute( key ) && toWrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) { - return false; - } - } - - // Check if styles can be merged. - for ( const key of wrapper.getStyleNames() ) { - if ( toWrap.hasStyle( key ) && toWrap.getStyle( key ) !== wrapper.getStyle( key ) ) { - return false; - } - } - - // Move all attributes/classes/styles from wrapper to wrapped AttributeElement. - for ( const key of wrapper.getAttributeKeys() ) { - // Classes and styles should be checked separately. - if ( key === 'class' || key === 'style' ) { - continue; - } - - // Move only these attributes that are not present - other are similar. - if ( !toWrap.hasAttribute( key ) ) { - toWrap.setAttribute( key, wrapper.getAttribute( key ) ); - } - } - - for ( const key of wrapper.getStyleNames() ) { - if ( !toWrap.hasStyle( key ) ) { - toWrap.setStyle( key, wrapper.getStyle( key ) ); - } - } - - for ( const key of wrapper.getClassNames() ) { - if ( !toWrap.hasClass( key ) ) { - toWrap.addClass( key ); - } - } - - return true; -} - // Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing corresponding attributes, // classes and styles. All attributes, classes and styles from wrapper should be present inside element being unwrapped. // diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index 23aa680f7..435de3fe5 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -1307,7 +1307,9 @@ describe( 'downcast-converters', () => { } ); viewContainer.setCustomProperty( 'removeHighlight', element => { - element.setAttribute( 'class', '' ); + controller.view.change( writer => { + writer.setAttribute( 'class', '', element ); + } ); } ); return viewContainer; diff --git a/tests/conversion/viewconsumable.js b/tests/conversion/viewconsumable.js index 1318a9bcb..f578d7058 100644 --- a/tests/conversion/viewconsumable.js +++ b/tests/conversion/viewconsumable.js @@ -474,8 +474,8 @@ describe( 'ViewConsumable', () => { } ); it( 'should add all attributes', () => { - el.setAttribute( 'title', 'foobar' ); - el.setAttribute( 'href', 'https://ckeditor.com' ); + el._setAttribute( 'title', 'foobar' ); + el._setAttribute( 'href', 'https://ckeditor.com' ); const consumables = ViewConsumable.consumablesFromElement( el ); expect( consumables.attribute.length ).to.equal( 2 ); diff --git a/tests/view/domconverter/view-to-dom.js b/tests/view/domconverter/view-to-dom.js index ab772f547..a10591aa8 100644 --- a/tests/view/domconverter/view-to-dom.js +++ b/tests/view/domconverter/view-to-dom.js @@ -29,9 +29,7 @@ describe( 'DomConverter', () => { it( 'should create tree of DOM elements from view elements', () => { const viewImg = new ViewElement( 'img' ); const viewText = new ViewText( 'foo' ); - const viewP = new ViewElement( 'p' ); - - viewP.setAttribute( 'class', 'foo' ); + const viewP = new ViewElement( 'p', { class: 'foo' } ); viewP.appendChildren( viewImg ); viewP.appendChildren( viewText ); @@ -59,9 +57,7 @@ describe( 'DomConverter', () => { it( 'should create tree of DOM elements from view elements and bind elements', () => { const viewImg = new ViewElement( 'img' ); const viewText = new ViewText( 'foo' ); - const viewP = new ViewElement( 'p' ); - - viewP.setAttribute( 'class', 'foo' ); + const viewP = new ViewElement( 'p', { class: 'foo' } ); viewP.appendChildren( viewImg ); viewP.appendChildren( viewText ); @@ -98,9 +94,7 @@ describe( 'DomConverter', () => { it( 'should create tree of DOM elements from view element without children', () => { const viewImg = new ViewElement( 'img' ); const viewText = new ViewText( 'foo' ); - const viewP = new ViewElement( 'p' ); - - viewP.setAttribute( 'class', 'foo' ); + const viewP = new ViewElement( 'p', { class: 'foo' } ); viewP.appendChildren( viewImg ); viewP.appendChildren( viewText ); diff --git a/tests/view/element.js b/tests/view/element.js index e986b63dc..74ee21153 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -247,8 +247,8 @@ describe( 'Element', () => { const other1 = el.clone(); const other2 = el.clone(); const other3 = el.clone(); - other1.setAttribute( 'baz', 'qux' ); - other2.setAttribute( 'foo', 'not-bar' ); + other1._setAttribute( 'baz', 'qux' ); + other2._setAttribute( 'foo', 'not-bar' ); other3.removeAttribute( 'foo' ); expect( el.isSimilar( other1 ) ).to.be.false; expect( el.isSimilar( other2 ) ).to.be.false; @@ -432,14 +432,14 @@ describe( 'Element', () => { describe( 'setAttribute', () => { it( 'should set attribute', () => { - el.setAttribute( 'foo', 'bar' ); + el._setAttribute( 'foo', 'bar' ); expect( el._attrs.has( 'foo' ) ).to.be.true; expect( el._attrs.get( 'foo' ) ).to.equal( 'bar' ); } ); it( 'should cast attribute value to a string', () => { - el.setAttribute( 'foo', true ); + el._setAttribute( 'foo', true ); expect( el._attrs.get( 'foo' ) ).to.equal( 'true' ); } ); @@ -450,11 +450,11 @@ describe( 'Element', () => { done(); } ); - el.setAttribute( 'foo', 'bar' ); + el._setAttribute( 'foo', 'bar' ); } ); it( 'should set class', () => { - el.setAttribute( 'class', 'foo bar' ); + el._setAttribute( 'class', 'foo bar' ); expect( el._attrs.has( 'class' ) ).to.be.false; expect( el._classes.has( 'foo' ) ).to.be.true; @@ -462,8 +462,8 @@ describe( 'Element', () => { } ); it( 'should replace all existing classes', () => { - el.setAttribute( 'class', 'foo bar baz' ); - el.setAttribute( 'class', 'qux' ); + el._setAttribute( 'class', 'foo bar baz' ); + el._setAttribute( 'class', 'qux' ); expect( el._classes.has( 'foo' ) ).to.be.false; expect( el._classes.has( 'bar' ) ).to.be.false; @@ -474,7 +474,7 @@ describe( 'Element', () => { it( 'should replace all styles', () => { el.setStyle( 'color', 'red' ); el.setStyle( 'top', '10px' ); - el.setAttribute( 'style', 'border:none' ); + el._setAttribute( 'style', 'border:none' ); expect( el.hasStyle( 'color' ) ).to.be.false; expect( el.hasStyle( 'top' ) ).to.be.false; @@ -485,7 +485,7 @@ describe( 'Element', () => { describe( 'getAttribute', () => { it( 'should return attribute', () => { - el.setAttribute( 'foo', 'bar' ); + el._setAttribute( 'foo', 'bar' ); expect( el.getAttribute( 'foo' ) ).to.equal( 'bar' ); expect( el.getAttribute( 'bom' ) ).to.not.be.ok; @@ -515,15 +515,15 @@ describe( 'Element', () => { describe( 'getAttributes', () => { it( 'should return attributes', () => { - el.setAttribute( 'foo', 'bar' ); - el.setAttribute( 'abc', 'xyz' ); + el._setAttribute( 'foo', 'bar' ); + el._setAttribute( 'abc', 'xyz' ); expect( Array.from( el.getAttributes() ) ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'abc', 'xyz' ] ] ); } ); it( 'should return class and style attribute', () => { - el.setAttribute( 'class', 'abc' ); - el.setAttribute( 'style', 'width:20px;' ); + el._setAttribute( 'class', 'abc' ); + el._setAttribute( 'style', 'width:20px;' ); el.addClass( 'xyz' ); el.setStyle( 'font-weight', 'bold' ); @@ -535,7 +535,7 @@ describe( 'Element', () => { describe( 'hasAttribute', () => { it( 'should return true if element has attribute', () => { - el.setAttribute( 'foo', 'bar' ); + el._setAttribute( 'foo', 'bar' ); expect( el.hasAttribute( 'foo' ) ).to.be.true; expect( el.hasAttribute( 'bom' ) ).to.be.false; @@ -556,8 +556,8 @@ describe( 'Element', () => { describe( 'getAttributeKeys', () => { it( 'should return keys', () => { - el.setAttribute( 'foo', true ); - el.setAttribute( 'bar', true ); + el._setAttribute( 'foo', true ); + el._setAttribute( 'bar', true ); const expected = [ 'foo', 'bar' ]; let i = 0; @@ -572,7 +572,7 @@ describe( 'Element', () => { it( 'should return class key', () => { el.addClass( 'foo' ); - el.setAttribute( 'bar', true ); + el._setAttribute( 'bar', true ); const expected = [ 'class', 'bar' ]; let i = 0; @@ -584,7 +584,7 @@ describe( 'Element', () => { it( 'should return style key', () => { el.setStyle( 'color', 'black' ); - el.setAttribute( 'bar', true ); + el._setAttribute( 'bar', true ); const expected = [ 'style', 'bar' ]; let i = 0; @@ -597,7 +597,7 @@ describe( 'Element', () => { describe( 'removeAttribute', () => { it( 'should remove attributes', () => { - el.setAttribute( 'foo', true ); + el._setAttribute( 'foo', true ); expect( el.hasAttribute( 'foo' ) ).to.be.true; @@ -609,7 +609,7 @@ describe( 'Element', () => { } ); it( 'should fire change event with attributes type', done => { - el.setAttribute( 'foo', 'bar' ); + el._setAttribute( 'foo', 'bar' ); el.once( 'change:attributes', eventInfo => { expect( eventInfo.source ).to.equal( el ); done(); diff --git a/tests/view/node.js b/tests/view/node.js index a17486909..bf60c0a53 100644 --- a/tests/view/node.js +++ b/tests/view/node.js @@ -303,8 +303,7 @@ describe( 'Node', () => { beforeEach( () => { text = new Text( 'foo' ); - img = new Element( 'img' ); - img.setAttribute( 'src', 'img.png' ); + img = new Element( 'img', { 'src': 'img.png' } ); root = new Element( 'p', { renderer: { markToSync: rootChangeSpy } } ); root.appendChildren( [ text, img ] ); @@ -323,14 +322,14 @@ describe( 'Node', () => { imgChangeSpy( 'attributes', node ); } ); - img.setAttribute( 'width', 100 ); + img._setAttribute( 'width', 100 ); sinon.assert.calledOnce( imgChangeSpy ); sinon.assert.calledWith( imgChangeSpy, 'attributes', img ); } ); it( 'should be fired on the parent', () => { - img.setAttribute( 'width', 100 ); + img._setAttribute( 'width', 100 ); sinon.assert.calledOnce( rootChangeSpy ); sinon.assert.calledWith( rootChangeSpy, 'attributes', img ); @@ -338,7 +337,7 @@ describe( 'Node', () => { describe( 'setAttribute()', () => { it( 'should fire change event', () => { - img.setAttribute( 'width', 100 ); + img._setAttribute( 'width', 100 ); sinon.assert.calledOnce( rootChangeSpy ); sinon.assert.calledWith( rootChangeSpy, 'attributes', img ); diff --git a/tests/view/renderer.js b/tests/view/renderer.js index 7d642b3d2..74d67d7e3 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -51,7 +51,7 @@ describe( 'Renderer', () => { } ); it( 'should mark attributes which need update', () => { - viewRoot.setAttribute( 'class', 'foo' ); + viewRoot._setAttribute( 'class', 'foo' ); renderer.markToSync( 'attributes', viewRoot ); @@ -151,7 +151,7 @@ describe( 'Renderer', () => { } ); it( 'should update attributes', () => { - viewRoot.setAttribute( 'class', 'foo' ); + viewRoot._setAttribute( 'class', 'foo' ); renderer.markToSync( 'attributes', viewRoot ); renderer.render(); @@ -162,7 +162,7 @@ describe( 'Renderer', () => { } ); it( 'should remove attributes', () => { - viewRoot.setAttribute( 'class', 'foo' ); + viewRoot._setAttribute( 'class', 'foo' ); domRoot.setAttribute( 'id', 'bar' ); domRoot.setAttribute( 'class', 'bar' ); diff --git a/tests/view/view/view.js b/tests/view/view/view.js index ae29e1b73..3620cb0c6 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -432,7 +432,7 @@ describe( 'view', () => { expect( domRoot.childNodes.length ).to.equal( 1 ); expect( domRoot.childNodes[ 0 ].getAttribute( 'class' ) ).to.equal( 'foo' ); - viewP.setAttribute( 'class', 'bar' ); + viewP._setAttribute( 'class', 'bar' ); view.render(); expect( domRoot.childNodes.length ).to.equal( 1 ); From 85c340004740f29f56a1f7ea5567773719e502c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 16:21:55 +0100 Subject: [PATCH 61/89] Using writer to remove attribute from view element. --- src/conversion/downcast-converters.js | 3 +- src/dev-utils/view.js | 2 +- src/view/element.js | 73 +++++++-------- src/view/placeholder.js | 2 +- src/view/writer.js | 124 ++++++++++++++------------ tests/view/element.js | 18 ++-- tests/view/node.js | 6 +- 7 files changed, 119 insertions(+), 109 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 9dee0b586..95cf71c43 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -726,7 +726,8 @@ export function changeAttribute( attributeCreator ) { const viewWriter = conversionApi.writer; if ( data.attributeOldValue !== null && oldAttribute ) { - mapper.toViewElement( data.item ).removeAttribute( oldAttribute.key ); + const viewElement = mapper.toViewElement( data.item ); + viewWriter.removeAttribute( oldAttribute.key, viewElement ); } // Then, if conversion was successful, set the new attribute. diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js index 02d1019c5..60bdebacc 100644 --- a/src/dev-utils/view.js +++ b/src/dev-utils/view.js @@ -952,7 +952,7 @@ function _convertElement( viewElement ) { function _convertElementNameAndPriority( viewElement ) { const parts = viewElement.name.split( ':' ); const priority = _convertPriority( viewElement.getAttribute( 'view-priority' ) ); - viewElement.removeAttribute( 'view-priority' ); + viewElement._removeAttribute( 'view-priority' ); if ( parts.length == 1 ) { return { diff --git a/src/view/element.js b/src/view/element.js index c20cac5fc..841138a3f 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -348,42 +348,6 @@ export default class Element extends Node { return count; } - /** - * Removes attribute from the element. - * - * @param {String} key Attribute key. - * @returns {Boolean} Returns true if an attribute existed and has been removed. - * @fires module:engine/view/node~Node#change - */ - removeAttribute( key ) { - this._fireChange( 'attributes', this ); - - // Remove class attribute. - if ( key == 'class' ) { - if ( this._classes.size > 0 ) { - this._classes.clear(); - - return true; - } - - return false; - } - - // Remove style attribute. - if ( key == 'style' ) { - if ( this._styles.size > 0 ) { - this._styles.clear(); - - return true; - } - - return false; - } - - // Remove other attributes. - return this._attrs.delete( key ); - } - /** * Removes number of child nodes starting at the given index and set the parent of these nodes to `null`. * @@ -712,6 +676,43 @@ export default class Element extends Node { } } + /** + * Removes attribute from the element. + * + * @protected + * @param {String} key Attribute key. + * @returns {Boolean} Returns true if an attribute existed and has been removed. + * @fires module:engine/view/node~Node#change + */ + _removeAttribute( key ) { + this._fireChange( 'attributes', this ); + + // Remove class attribute. + if ( key == 'class' ) { + if ( this._classes.size > 0 ) { + this._classes.clear(); + + return true; + } + + return false; + } + + // Remove style attribute. + if ( key == 'style' ) { + if ( this._styles.size > 0 ) { + this._styles.clear(); + + return true; + } + + return false; + } + + // Remove other attributes. + return this._attrs.delete( key ); + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/placeholder.js b/src/view/placeholder.js index a6b411a3d..3e42cb917 100644 --- a/src/view/placeholder.js +++ b/src/view/placeholder.js @@ -60,7 +60,7 @@ export function detachPlaceholder( element ) { const document = element.document; element.removeClass( 'ck-placeholder' ); - element.removeAttribute( 'data-placeholder' ); + element._removeAttribute( 'data-placeholder' ); if ( documentPlaceholders.has( document ) ) { documentPlaceholders.get( document ).delete( element ); diff --git a/src/view/writer.js b/src/view/writer.js index f5c3ddc4e..563d5499e 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -186,6 +186,10 @@ export default class Writer { element._setAttribute( key, value ); } + removeAttribute( key, element ) { + element._removeAttribute( key ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. @@ -681,7 +685,7 @@ export default class Writer { const node = breakStart.nodeAfter; // Unwrap single attribute element. - if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && unwrapAttributeElement( attribute, node ) ) { + if ( !attribute.isSimilar( node ) && node instanceof AttributeElement && this._unwrapAttributeElement( attribute, node ) ) { const start = this.mergeAttributes( breakStart ); if ( !start.isEqual( breakStart ) ) { @@ -1047,6 +1051,67 @@ export default class Writer { return true; } + + /** + * Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing + * corresponding attributes, classes and styles. All attributes, classes and styles from wrapper should be present + * inside element being unwrapped. + * + * @private + * @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement. + * @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element. + * @returns {Boolean} Returns `true` if elements are unwrapped. + **/ + _unwrapAttributeElement( wrapper, toUnwrap ) { + // Can't unwrap if name or priority differs. + if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) { + return false; + } + + // Check if AttributeElement has all wrapper attributes. + for ( const key of wrapper.getAttributeKeys() ) { + // Classes and styles should be checked separately. + if ( key === 'class' || key === 'style' ) { + continue; + } + + // If some attributes are missing or different we cannot unwrap. + if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) { + return false; + } + } + + // Check if AttributeElement has all wrapper classes. + if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) { + return false; + } + + // Check if AttributeElement has all wrapper styles. + for ( const key of wrapper.getStyleNames() ) { + // If some styles are missing or different we cannot unwrap. + if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) { + return false; + } + } + + // Remove all wrapper's attributes from unwrapped element. + for ( const key of wrapper.getAttributeKeys() ) { + // Classes and styles should be checked separately. + if ( key === 'class' || key === 'style' ) { + continue; + } + + this.removeAttribute( key, toUnwrap ); + } + + // Remove all wrapper's classes from unwrapped element. + toUnwrap.removeClass( ...wrapper.getClassNames() ); + + // Remove all wrapper's styles from unwrapped element. + toUnwrap.removeStyle( ...wrapper.getStyleNames() ); + + return true; + } } // Helper function for `view.writer.wrap`. Checks if given element has any children that are not ui elements. @@ -1298,63 +1363,6 @@ function mergeTextNodes( t1, t2 ) { return new Position( t1, nodeBeforeLength ); } -// Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing corresponding attributes, -// classes and styles. All attributes, classes and styles from wrapper should be present inside element being unwrapped. -// -// @param {module:engine/view/attributeelement~AttributeElement} wrapper Wrapper AttributeElement. -// @param {module:engine/view/attributeelement~AttributeElement} toUnwrap AttributeElement to unwrap using wrapper element. -// @returns {Boolean} Returns `true` if elements are unwrapped. -function unwrapAttributeElement( wrapper, toUnwrap ) { - // Can't unwrap if name or priority differs. - if ( wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority ) { - return false; - } - - // Check if AttributeElement has all wrapper attributes. - for ( const key of wrapper.getAttributeKeys() ) { - // Classes and styles should be checked separately. - if ( key === 'class' || key === 'style' ) { - continue; - } - - // If some attributes are missing or different we cannot unwrap. - if ( !toUnwrap.hasAttribute( key ) || toUnwrap.getAttribute( key ) !== wrapper.getAttribute( key ) ) { - return false; - } - } - - // Check if AttributeElement has all wrapper classes. - if ( !toUnwrap.hasClass( ...wrapper.getClassNames() ) ) { - return false; - } - - // Check if AttributeElement has all wrapper styles. - for ( const key of wrapper.getStyleNames() ) { - // If some styles are missing or different we cannot unwrap. - if ( !toUnwrap.hasStyle( key ) || toUnwrap.getStyle( key ) !== wrapper.getStyle( key ) ) { - return false; - } - } - - // Remove all wrapper's attributes from unwrapped element. - for ( const key of wrapper.getAttributeKeys() ) { - // Classes and styles should be checked separately. - if ( key === 'class' || key === 'style' ) { - continue; - } - - toUnwrap.removeAttribute( key ); - } - - // Remove all wrapper's classes from unwrapped element. - toUnwrap.removeClass( ...wrapper.getClassNames() ); - - // Remove all wrapper's styles from unwrapped element. - toUnwrap.removeStyle( ...wrapper.getStyleNames() ); - - return true; -} - // Returns `true` if range is located in same {@link module:engine/view/attributeelement~AttributeElement AttributeElement} // (`start` and `end` positions are located inside same {@link module:engine/view/attributeelement~AttributeElement AttributeElement}), // starts on 0 offset and ends after last child node. diff --git a/tests/view/element.js b/tests/view/element.js index 74ee21153..ca9280519 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -249,7 +249,7 @@ describe( 'Element', () => { const other3 = el.clone(); other1._setAttribute( 'baz', 'qux' ); other2._setAttribute( 'foo', 'not-bar' ); - other3.removeAttribute( 'foo' ); + other3._removeAttribute( 'foo' ); expect( el.isSimilar( other1 ) ).to.be.false; expect( el.isSimilar( other2 ) ).to.be.false; expect( el.isSimilar( other3 ) ).to.be.false; @@ -430,7 +430,7 @@ describe( 'Element', () => { el = new Element( 'p' ); } ); - describe( 'setAttribute', () => { + describe( '_setAttribute', () => { it( 'should set attribute', () => { el._setAttribute( 'foo', 'bar' ); @@ -595,13 +595,13 @@ describe( 'Element', () => { } ); } ); - describe( 'removeAttribute', () => { + describe( '_removeAttribute', () => { it( 'should remove attributes', () => { el._setAttribute( 'foo', true ); expect( el.hasAttribute( 'foo' ) ).to.be.true; - el.removeAttribute( 'foo' ); + el._removeAttribute( 'foo' ); expect( el.hasAttribute( 'foo' ) ).to.be.false; @@ -615,14 +615,14 @@ describe( 'Element', () => { done(); } ); - el.removeAttribute( 'foo' ); + el._removeAttribute( 'foo' ); } ); it( 'should remove class attribute', () => { el.addClass( 'foo', 'bar' ); const el2 = new Element( 'p' ); - const removed1 = el.removeAttribute( 'class' ); - const removed2 = el2.removeAttribute( 'class' ); + const removed1 = el._removeAttribute( 'class' ); + const removed2 = el2._removeAttribute( 'class' ); expect( el.hasAttribute( 'class' ) ).to.be.false; expect( el.hasClass( 'foo' ) ).to.be.false; @@ -635,8 +635,8 @@ describe( 'Element', () => { el.setStyle( 'color', 'red' ); el.setStyle( 'position', 'fixed' ); const el2 = new Element( 'p' ); - const removed1 = el.removeAttribute( 'style' ); - const removed2 = el2.removeAttribute( 'style' ); + const removed1 = el._removeAttribute( 'style' ); + const removed2 = el2._removeAttribute( 'style' ); expect( el.hasAttribute( 'style' ) ).to.be.false; expect( el.hasStyle( 'color' ) ).to.be.false; diff --git a/tests/view/node.js b/tests/view/node.js index bf60c0a53..564e884cb 100644 --- a/tests/view/node.js +++ b/tests/view/node.js @@ -335,7 +335,7 @@ describe( 'Node', () => { sinon.assert.calledWith( rootChangeSpy, 'attributes', img ); } ); - describe( 'setAttribute()', () => { + describe( '_setAttribute()', () => { it( 'should fire change event', () => { img._setAttribute( 'width', 100 ); @@ -344,9 +344,9 @@ describe( 'Node', () => { } ); } ); - describe( 'removeAttribute()', () => { + describe( '_removeAttribute()', () => { it( 'should fire change event', () => { - img.removeAttribute( 'src' ); + img._removeAttribute( 'src' ); sinon.assert.calledOnce( rootChangeSpy ); sinon.assert.calledWith( rootChangeSpy, 'attributes', img ); From ad65a63fa901a250d208aaff1d7056e7e2bc34c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 16:57:29 +0100 Subject: [PATCH 62/89] Using writer to set class on view element. --- src/conversion/downcast-converters.js | 11 +---- src/view/element.js | 31 +++++++------ src/view/placeholder.js | 4 +- src/view/writer.js | 6 ++- tests/conversion/downcast-converters.js | 4 +- .../downcast-selection-converters.js | 2 +- tests/conversion/viewconsumable.js | 2 +- tests/view/element.js | 44 +++++++++---------- tests/view/matcher.js | 8 ++-- 9 files changed, 57 insertions(+), 55 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 95cf71c43..5d2690167 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -386,13 +386,7 @@ function _createViewElementFromDefinition( viewElementDefinition, ViewElementCla } if ( viewElementDefinition.class ) { - const classes = viewElementDefinition.class; - - if ( typeof classes == 'string' ) { - element.addClass( classes ); - } else { - element.addClass( ...classes ); - } + element._addClass( viewElementDefinition.class ); } return element; @@ -1011,8 +1005,7 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { const viewElement = new HighlightAttributeElement( 'span', descriptor.attributes ); if ( descriptor.class ) { - const cssClasses = Array.isArray( descriptor.class ) ? descriptor.class : [ descriptor.class ]; - viewElement.addClass( ...cssClasses ); + viewElement._addClass( descriptor.class ); } if ( descriptor.priority ) { diff --git a/src/view/element.js b/src/view/element.js index 841138a3f..70009d79b 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -419,20 +419,6 @@ export default class Element extends Node { return true; } - /** - * Adds specified class. - * - * element.addClass( 'foo' ); // Adds 'foo' class. - * element.addClass( 'foo', 'bar' ); // Adds 'foo' and 'bar' classes. - * - * @param {...String} className - * @fires module:engine/view/node~Node#change - */ - addClass( ...className ) { - this._fireChange( 'attributes', this ); - className.forEach( name => this._classes.add( name ) ); - } - /** * Removes specified class. * @@ -713,6 +699,23 @@ export default class Element extends Node { return this._attrs.delete( key ); } + /** + * Adds specified class. + * + * element._addClass( 'foo' ); // Adds 'foo' class. + * element._addClass( [ 'foo', 'bar' ] ); // Adds 'foo' and 'bar' classes. + * + * @protected + * @param {Array.|String} className + * @fires module:engine/view/node~Node#change + */ + _addClass( className ) { + this._fireChange( 'attributes', this ); + + className = Array.isArray( className ) ? className : [ className ]; + className.forEach( name => this._classes.add( name ) ); + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/placeholder.js b/src/view/placeholder.js index 3e42cb917..538b97134 100644 --- a/src/view/placeholder.js +++ b/src/view/placeholder.js @@ -108,14 +108,14 @@ function updateSinglePlaceholder( element, checkFunction ) { // If element is empty and editor is blurred. if ( !document.isFocused && isEmptyish ) { - element.addClass( 'ck-placeholder' ); + element._addClass( 'ck-placeholder' ); return; } // It there are no child elements and selection is not placed inside element. if ( isEmptyish && anchor && anchor.parent !== element ) { - element.addClass( 'ck-placeholder' ); + element._addClass( 'ck-placeholder' ); } else { element.removeClass( 'ck-placeholder' ); } diff --git a/src/view/writer.js b/src/view/writer.js index 563d5499e..9adad3e6a 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -190,6 +190,10 @@ export default class Writer { element._removeAttribute( key ); } + addClass( className, element ) { + element._addClass( className ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. @@ -1045,7 +1049,7 @@ export default class Writer { for ( const key of wrapper.getClassNames() ) { if ( !toWrap.hasClass( key ) ) { - toWrap.addClass( key ); + this.addClass( key, toWrap ); } } diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index 435de3fe5..50b37e13f 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -1303,7 +1303,9 @@ describe( 'downcast-converters', () => { const viewContainer = new ViewContainerElement( 'div' ); viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => { - element.addClass( descriptor.class ); + controller.view.change( writer => { + writer.addClass( descriptor.class, element ); + } ); } ); viewContainer.setCustomProperty( 'removeHighlight', element => { diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index d3caad96a..48c2c81c2 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -523,7 +523,7 @@ describe( 'downcast-selection-converters', () => { consumable.consume( selection, 'selection' ); const viewNode = conversionApi.mapper.toViewElement( node ); - viewNode.addClass( 'selected' ); + conversionApi.writer.addClass( 'selected', viewNode ); } } } ); diff --git a/tests/conversion/viewconsumable.js b/tests/conversion/viewconsumable.js index f578d7058..2bb0d0d79 100644 --- a/tests/conversion/viewconsumable.js +++ b/tests/conversion/viewconsumable.js @@ -487,7 +487,7 @@ describe( 'ViewConsumable', () => { } ); it( 'should add all classes', () => { - el.addClass( 'foo', 'bar', 'baz' ); + el._addClass( [ 'foo', 'bar', 'baz' ] ); const consumables = ViewConsumable.consumablesFromElement( el ); expect( consumables.class.length ).to.equal( 3 ); diff --git a/tests/view/element.js b/tests/view/element.js index ca9280519..8c9364aee 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -174,7 +174,7 @@ describe( 'Element', () => { it( 'should clone class attribute', () => { const el = new Element( 'p', { foo: 'bar' } ); - el.addClass( 'baz', 'qux' ); + el._addClass( [ 'baz', 'qux' ] ); const clone = el.clone( false ); expect( clone ).to.not.equal( el ); @@ -261,10 +261,10 @@ describe( 'Element', () => { const el3 = new Element( 'p' ); const el4 = new Element( 'p' ); - el1.addClass( 'foo', 'bar' ); - el2.addClass( 'bar', 'foo' ); - el3.addClass( 'baz' ); - el4.addClass( 'baz', 'bar' ); + el1._addClass( [ 'foo', 'bar' ] ); + el2._addClass( [ 'bar', 'foo' ] ); + el3._addClass( 'baz' ); + el4._addClass( [ 'baz', 'bar' ] ); expect( el1.isSimilar( el2 ) ).to.be.true; expect( el1.isSimilar( el3 ) ).to.be.false; @@ -492,7 +492,7 @@ describe( 'Element', () => { } ); it( 'should return class attribute', () => { - el.addClass( 'foo', 'bar' ); + el._addClass( [ 'foo', 'bar' ] ); expect( el.getAttribute( 'class' ) ).to.equal( 'foo bar' ); } ); @@ -524,7 +524,7 @@ describe( 'Element', () => { it( 'should return class and style attribute', () => { el._setAttribute( 'class', 'abc' ); el._setAttribute( 'style', 'width:20px;' ); - el.addClass( 'xyz' ); + el._addClass( 'xyz' ); el.setStyle( 'font-weight', 'bold' ); expect( Array.from( el.getAttributes() ) ).to.deep.equal( [ @@ -543,7 +543,7 @@ describe( 'Element', () => { it( 'should return true if element has class attribute', () => { expect( el.hasAttribute( 'class' ) ).to.be.false; - el.addClass( 'foo' ); + el._addClass( 'foo' ); expect( el.hasAttribute( 'class' ) ).to.be.true; } ); @@ -571,7 +571,7 @@ describe( 'Element', () => { } ); it( 'should return class key', () => { - el.addClass( 'foo' ); + el._addClass( 'foo' ); el._setAttribute( 'bar', true ); const expected = [ 'class', 'bar' ]; let i = 0; @@ -619,7 +619,7 @@ describe( 'Element', () => { } ); it( 'should remove class attribute', () => { - el.addClass( 'foo', 'bar' ); + el._addClass( [ 'foo', 'bar' ] ); const el2 = new Element( 'p' ); const removed1 = el._removeAttribute( 'class' ); const removed2 = el2._removeAttribute( 'class' ); @@ -654,9 +654,9 @@ describe( 'Element', () => { el = new Element( 'p' ); } ); - describe( 'addClass', () => { + describe( '_addClass()', () => { it( 'should add single class', () => { - el.addClass( 'one' ); + el._addClass( 'one' ); expect( el._classes.has( 'one' ) ).to.be.true; } ); @@ -667,11 +667,11 @@ describe( 'Element', () => { done(); } ); - el.addClass( 'one' ); + el._addClass( 'one' ); } ); it( 'should add multiple classes', () => { - el.addClass( 'one', 'two', 'three' ); + el._addClass( [ 'one', 'two', 'three' ] ); expect( el._classes.has( 'one' ) ).to.be.true; expect( el._classes.has( 'two' ) ).to.be.true; @@ -681,7 +681,7 @@ describe( 'Element', () => { describe( 'removeClass', () => { it( 'should remove single class', () => { - el.addClass( 'one', 'two', 'three' ); + el._addClass( [ 'one', 'two', 'three' ] ); el.removeClass( 'one' ); @@ -691,7 +691,7 @@ describe( 'Element', () => { } ); it( 'should fire change event with attributes type', done => { - el.addClass( 'one' ); + el._addClass( 'one' ); el.once( 'change:attributes', eventInfo => { expect( eventInfo.source ).to.equal( el ); done(); @@ -701,7 +701,7 @@ describe( 'Element', () => { } ); it( 'should remove multiple classes', () => { - el.addClass( 'one', 'two', 'three', 'four' ); + el._addClass( [ 'one', 'two', 'three', 'four' ] ); el.removeClass( 'one', 'two', 'three' ); expect( el._classes.has( 'one' ) ).to.be.false; @@ -713,7 +713,7 @@ describe( 'Element', () => { describe( 'hasClass', () => { it( 'should check if element has a class', () => { - el.addClass( 'one', 'two', 'three' ); + el._addClass( [ 'one', 'two', 'three' ] ); expect( el.hasClass( 'one' ) ).to.be.true; expect( el.hasClass( 'two' ) ).to.be.true; @@ -722,7 +722,7 @@ describe( 'Element', () => { } ); it( 'should check if element has multiple classes', () => { - el.addClass( 'one', 'two', 'three' ); + el._addClass( [ 'one', 'two', 'three' ] ); expect( el.hasClass( 'one', 'two' ) ).to.be.true; expect( el.hasClass( 'three', 'two' ) ).to.be.true; @@ -734,7 +734,7 @@ describe( 'Element', () => { describe( 'getClassNames', () => { it( 'should return iterator with all class names', () => { const names = [ 'one', 'two', 'three' ]; - el.addClass( ...names ); + el._addClass( names ); const iterator = el.getClassNames(); let i = 0; @@ -1019,7 +1019,7 @@ describe( 'Element', () => { it( 'should return classes in sorted order', () => { const el = new Element( 'fruit' ); - el.addClass( 'banana', 'lemon', 'apple' ); + el._addClass( [ 'banana', 'lemon', 'apple' ] ); expect( el.getIdentity() ).to.equal( 'fruit class="apple,banana,lemon"' ); } ); @@ -1049,7 +1049,7 @@ describe( 'Element', () => { style: 'text-align:center;border-radius:10px' } ); - el.addClass( 'three', 'two', 'one' ); + el._addClass( [ 'three', 'two', 'one' ] ); expect( el.getIdentity() ).to.equal( 'baz class="one,three,two" style="border-radius:10px;text-align:center" bar="two" foo="one"' diff --git a/tests/view/matcher.js b/tests/view/matcher.js index af62c219d..99c94449e 100644 --- a/tests/view/matcher.js +++ b/tests/view/matcher.js @@ -306,7 +306,7 @@ describe( 'Matcher', () => { }; const matcher = new Matcher( pattern ); const el = new Element( 'a' ); - el.addClass( 'foo', 'bar', 'baz' ); + el._addClass( [ 'foo', 'bar', 'baz' ] ); const result = matcher.match( el ); expect( result ).to.be.an( 'object' ); @@ -376,9 +376,9 @@ describe( 'Matcher', () => { const el2 = new Element( 'p' ); const el3 = new Element( 'p' ); - el1.addClass( 'red-foreground' ); - el2.addClass( 'red-background' ); - el3.addClass( 'blue-text' ); + el1._addClass( 'red-foreground' ); + el2._addClass( 'red-background' ); + el3._addClass( 'blue-text' ); const result = matcher.matchAll( el1, el2, el3 ); expect( result ).to.be.an( 'array' ); From d1d022c6ba30a6d4ffb7296fb6e14cea8aa791b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 17:19:43 +0100 Subject: [PATCH 63/89] Using writer to remove class from view element. --- src/view/element.js | 30 ++++++++++++++++-------------- src/view/placeholder.js | 6 +++--- src/view/writer.js | 6 +++++- tests/view/element.js | 8 ++++---- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/view/element.js b/src/view/element.js index 70009d79b..956275d06 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -419,20 +419,6 @@ export default class Element extends Node { return true; } - /** - * Removes specified class. - * - * element.removeClass( 'foo' ); // Removes 'foo' class. - * element.removeClass( 'foo', 'bar' ); // Removes both 'foo' and 'bar' classes. - * - * @param {...String} className - * @fires module:engine/view/node~Node#change - */ - removeClass( ...className ) { - this._fireChange( 'attributes', this ); - className.forEach( name => this._classes.delete( name ) ); - } - /** * Returns true if class is present. * If more then one class is provided - returns true only when all classes are present. @@ -716,6 +702,22 @@ export default class Element extends Node { className.forEach( name => this._classes.add( name ) ); } + /** + * Removes specified class. + * + * element._removeClass( 'foo' ); // Removes 'foo' class. + * element._removeClass( [ 'foo', 'bar' ] ); // Removes both 'foo' and 'bar' classes. + * + * @param {Array.|String} className + * @fires module:engine/view/node~Node#change + */ + _removeClass( className ) { + this._fireChange( 'attributes', this ); + + className = Array.isArray( className ) ? className : [ className ]; + className.forEach( name => this._classes.delete( name ) ); + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/placeholder.js b/src/view/placeholder.js index 538b97134..80c5e61bb 100644 --- a/src/view/placeholder.js +++ b/src/view/placeholder.js @@ -59,7 +59,7 @@ export function attachPlaceholder( view, element, placeholderText, checkFunction export function detachPlaceholder( element ) { const document = element.document; - element.removeClass( 'ck-placeholder' ); + element._removeClass( 'ck-placeholder' ); element._removeAttribute( 'data-placeholder' ); if ( documentPlaceholders.has( document ) ) { @@ -97,7 +97,7 @@ function updateSinglePlaceholder( element, checkFunction ) { // If checkFunction is provided and returns false - remove placeholder. if ( checkFunction && !checkFunction() ) { - element.removeClass( 'ck-placeholder' ); + element._removeClass( 'ck-placeholder' ); return; } @@ -117,6 +117,6 @@ function updateSinglePlaceholder( element, checkFunction ) { if ( isEmptyish && anchor && anchor.parent !== element ) { element._addClass( 'ck-placeholder' ); } else { - element.removeClass( 'ck-placeholder' ); + element._removeClass( 'ck-placeholder' ); } } diff --git a/src/view/writer.js b/src/view/writer.js index 9adad3e6a..c17d8419f 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -194,6 +194,10 @@ export default class Writer { element._addClass( className ); } + removeClass( className, element ) { + element._removeClass( className ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. @@ -1109,7 +1113,7 @@ export default class Writer { } // Remove all wrapper's classes from unwrapped element. - toUnwrap.removeClass( ...wrapper.getClassNames() ); + this.removeClass( Array.from( wrapper.getClassNames() ), toUnwrap ); // Remove all wrapper's styles from unwrapped element. toUnwrap.removeStyle( ...wrapper.getStyleNames() ); diff --git a/tests/view/element.js b/tests/view/element.js index 8c9364aee..11609bc50 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -679,11 +679,11 @@ describe( 'Element', () => { } ); } ); - describe( 'removeClass', () => { + describe( '_removeClass()', () => { it( 'should remove single class', () => { el._addClass( [ 'one', 'two', 'three' ] ); - el.removeClass( 'one' ); + el._removeClass( 'one' ); expect( el._classes.has( 'one' ) ).to.be.false; expect( el._classes.has( 'two' ) ).to.be.true; @@ -697,12 +697,12 @@ describe( 'Element', () => { done(); } ); - el.removeClass( 'one' ); + el._removeClass( 'one' ); } ); it( 'should remove multiple classes', () => { el._addClass( [ 'one', 'two', 'three', 'four' ] ); - el.removeClass( 'one', 'two', 'three' ); + el._removeClass( [ 'one', 'two', 'three' ] ); expect( el._classes.has( 'one' ) ).to.be.false; expect( el._classes.has( 'two' ) ).to.be.false; From 8c8a4c5c15540bd7ebd807836be1cfc605a7bdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 17:28:11 +0100 Subject: [PATCH 64/89] Using writer to set styles to view element. --- src/conversion/downcast-converters.js | 2 +- src/view/element.js | 55 ++++++++++++++------------- src/view/writer.js | 6 ++- tests/conversion/viewconsumable.js | 2 +- tests/view/element.js | 54 +++++++++++++------------- tests/view/matcher.js | 2 +- 6 files changed, 63 insertions(+), 58 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 5d2690167..a64bcd526 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -382,7 +382,7 @@ function _createViewElementFromDefinition( viewElementDefinition, ViewElementCla const element = new ViewElementClass( viewElementDefinition.name, Object.assign( {}, viewElementDefinition.attribute ) ); if ( viewElementDefinition.style ) { - element.setStyle( viewElementDefinition.style ); + element._setStyle( viewElementDefinition.style ); } if ( viewElementDefinition.class ) { diff --git a/src/view/element.js b/src/view/element.js index 956275d06..321ef34af 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -447,33 +447,6 @@ export default class Element extends Node { return this._classes.keys(); } - /** - * Adds style to the element. - * - * element.setStyle( 'color', 'red' ); - * element.setStyle( { - * color: 'red', - * position: 'fixed' - * } ); - * - * @param {String|Object} property Property name or object with key - value pairs. - * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter. - * @fires module:engine/view/node~Node#change - */ - setStyle( property, value ) { - this._fireChange( 'attributes', this ); - - if ( isPlainObject( property ) ) { - const keys = Object.keys( property ); - - for ( const key of keys ) { - this._styles.set( key, property[ key ] ); - } - } else { - this._styles.set( property, value ); - } - } - /** * Returns style value for given property. * Undefined is returned if style does not exist. @@ -718,6 +691,34 @@ export default class Element extends Node { className.forEach( name => this._classes.delete( name ) ); } + /** + * Adds style to the element. + * + * element._setStyle( 'color', 'red' ); + * element._setStyle( { + * color: 'red', + * position: 'fixed' + * } ); + * + * @protected + * @param {String|Object} property Property name or object with key - value pairs. + * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter. + * @fires module:engine/view/node~Node#change + */ + _setStyle( property, value ) { + this._fireChange( 'attributes', this ); + + if ( isPlainObject( property ) ) { + const keys = Object.keys( property ); + + for ( const key of keys ) { + this._styles.set( key, property[ key ] ); + } + } else { + this._styles.set( property, value ); + } + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/writer.js b/src/view/writer.js index c17d8419f..898954e68 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -198,6 +198,10 @@ export default class Writer { element._removeClass( className ); } + setStyle( property, value, element ) { + element._setStyle( property, value ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. @@ -1047,7 +1051,7 @@ export default class Writer { for ( const key of wrapper.getStyleNames() ) { if ( !toWrap.hasStyle( key ) ) { - toWrap.setStyle( key, wrapper.getStyle( key ) ); + this.setStyle( key, wrapper.getStyle( key ), toWrap ); } } diff --git a/tests/conversion/viewconsumable.js b/tests/conversion/viewconsumable.js index 2bb0d0d79..9ba4ff3c1 100644 --- a/tests/conversion/viewconsumable.js +++ b/tests/conversion/viewconsumable.js @@ -500,7 +500,7 @@ describe( 'ViewConsumable', () => { } ); it( 'should add all styles', () => { - el.setStyle( { + el._setStyle( { color: 'red', position: 'absolute' } ); diff --git a/tests/view/element.js b/tests/view/element.js index 11609bc50..5374a9e40 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -277,13 +277,13 @@ describe( 'Element', () => { const el3 = new Element( 'p' ); const el4 = new Element( 'p' ); - el1.setStyle( 'color', 'red' ); - el1.setStyle( 'top', '10px' ); - el2.setStyle( 'top', '20px' ); - el3.setStyle( 'top', '10px' ); - el3.setStyle( 'color', 'red' ); - el4.setStyle( 'color', 'blue' ); - el4.setStyle( 'top', '10px' ); + el1._setStyle( 'color', 'red' ); + el1._setStyle( 'top', '10px' ); + el2._setStyle( 'top', '20px' ); + el3._setStyle( 'top', '10px' ); + el3._setStyle( 'color', 'red' ); + el4._setStyle( 'color', 'blue' ); + el4._setStyle( 'top', '10px' ); expect( el1.isSimilar( el2 ) ).to.be.false; expect( el1.isSimilar( el3 ) ).to.be.true; @@ -472,8 +472,8 @@ describe( 'Element', () => { } ); it( 'should replace all styles', () => { - el.setStyle( 'color', 'red' ); - el.setStyle( 'top', '10px' ); + el._setStyle( 'color', 'red' ); + el._setStyle( 'top', '10px' ); el._setAttribute( 'style', 'border:none' ); expect( el.hasStyle( 'color' ) ).to.be.false; @@ -502,8 +502,8 @@ describe( 'Element', () => { } ); it( 'should return style attribute', () => { - el.setStyle( 'color', 'red' ); - el.setStyle( 'top', '10px' ); + el._setStyle( 'color', 'red' ); + el._setStyle( 'top', '10px' ); expect( el.getAttribute( 'style' ) ).to.equal( 'color:red;top:10px;' ); } ); @@ -525,7 +525,7 @@ describe( 'Element', () => { el._setAttribute( 'class', 'abc' ); el._setAttribute( 'style', 'width:20px;' ); el._addClass( 'xyz' ); - el.setStyle( 'font-weight', 'bold' ); + el._setStyle( 'font-weight', 'bold' ); expect( Array.from( el.getAttributes() ) ).to.deep.equal( [ [ 'class', 'abc xyz' ], [ 'style', 'width:20px;font-weight:bold;' ] @@ -549,7 +549,7 @@ describe( 'Element', () => { it( 'should return true if element has style attribute', () => { expect( el.hasAttribute( 'style' ) ).to.be.false; - el.setStyle( 'border', '1px solid red' ); + el._setStyle( 'border', '1px solid red' ); expect( el.hasAttribute( 'style' ) ).to.be.true; } ); } ); @@ -583,7 +583,7 @@ describe( 'Element', () => { } ); it( 'should return style key', () => { - el.setStyle( 'color', 'black' ); + el._setStyle( 'color', 'black' ); el._setAttribute( 'bar', true ); const expected = [ 'style', 'bar' ]; let i = 0; @@ -632,8 +632,8 @@ describe( 'Element', () => { } ); it( 'should remove style attribute', () => { - el.setStyle( 'color', 'red' ); - el.setStyle( 'position', 'fixed' ); + el._setStyle( 'color', 'red' ); + el._setStyle( 'position', 'fixed' ); const el2 = new Element( 'p' ); const removed1 = el._removeAttribute( 'style' ); const removed2 = el2._removeAttribute( 'style' ); @@ -752,9 +752,9 @@ describe( 'Element', () => { el = new Element( 'p' ); } ); - describe( 'setStyle', () => { + describe( '_setStyle()', () => { it( 'should set element style', () => { - el.setStyle( 'color', 'red' ); + el._setStyle( 'color', 'red' ); expect( el._styles.has( 'color' ) ).to.be.true; expect( el._styles.get( 'color' ) ).to.equal( 'red' ); @@ -766,11 +766,11 @@ describe( 'Element', () => { done(); } ); - el.setStyle( 'color', 'red' ); + el._setStyle( 'color', 'red' ); } ); it( 'should set multiple styles by providing an object', () => { - el.setStyle( { + el._setStyle( { color: 'red', position: 'fixed' } ); @@ -784,7 +784,7 @@ describe( 'Element', () => { describe( 'getStyle', () => { it( 'should get style', () => { - el.setStyle( { + el._setStyle( { color: 'red', border: '1px solid red' } ); @@ -798,7 +798,7 @@ describe( 'Element', () => { it( 'should return iterator with all style names', () => { const names = [ 'color', 'position' ]; - el.setStyle( { + el._setStyle( { color: 'red', position: 'absolute' } ); @@ -814,14 +814,14 @@ describe( 'Element', () => { describe( 'hasStyle', () => { it( 'should check if element has a style', () => { - el.setStyle( 'padding-top', '10px' ); + el._setStyle( 'padding-top', '10px' ); expect( el.hasStyle( 'padding-top' ) ).to.be.true; expect( el.hasStyle( 'padding-left' ) ).to.be.false; } ); it( 'should check if element has multiple styles', () => { - el.setStyle( { + el._setStyle( { 'padding-top': '10px', 'margin-left': '10px', 'color': '10px;' @@ -835,14 +835,14 @@ describe( 'Element', () => { describe( 'removeStyle', () => { it( 'should remove style', () => { - el.setStyle( 'padding-top', '10px' ); + el._setStyle( 'padding-top', '10px' ); el.removeStyle( 'padding-top' ); expect( el.hasStyle( 'padding-top' ) ).to.be.false; } ); it( 'should fire change event with attributes type', done => { - el.setStyle( 'color', 'red' ); + el._setStyle( 'color', 'red' ); el.once( 'change:attributes', eventInfo => { expect( eventInfo.source ).to.equal( el ); done(); @@ -852,7 +852,7 @@ describe( 'Element', () => { } ); it( 'should remove multiple styles', () => { - el.setStyle( { + el._setStyle( { 'padding-top': '10px', 'margin-top': '10px', 'color': 'red' diff --git a/tests/view/matcher.js b/tests/view/matcher.js index 99c94449e..9d222d476 100644 --- a/tests/view/matcher.js +++ b/tests/view/matcher.js @@ -328,7 +328,7 @@ describe( 'Matcher', () => { }; const matcher = new Matcher( pattern ); const el = new Element( 'a' ); - el.setStyle( { + el._setStyle( { color: 'red', position: 'relative' } ); From c4c4d3dfa9c7087bc59d43897f0093a68675321c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 18:40:50 +0100 Subject: [PATCH 65/89] Using writer to remove styles from view element. --- src/view/element.js | 31 +++++++++++++++++-------------- src/view/writer.js | 6 +++++- tests/view/element.js | 8 ++++---- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/view/element.js b/src/view/element.js index 321ef34af..d5f962160 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -486,20 +486,6 @@ export default class Element extends Node { return true; } - /** - * Removes specified style. - * - * element.removeStyle( 'color' ); // Removes 'color' style. - * element.removeStyle( 'color', 'border-top' ); // Removes both 'color' and 'border-top' styles. - * - * @param {...String} property - * @fires module:engine/view/node~Node#change - */ - removeStyle( ...property ) { - this._fireChange( 'attributes', this ); - property.forEach( name => this._styles.delete( name ) ); - } - /** * Returns ancestor element that match specified pattern. * Provided patterns should be compatible with {@link module:engine/view/matcher~Matcher Matcher} as it is used internally. @@ -719,6 +705,23 @@ export default class Element extends Node { } } + /** + * Removes specified style. + * + * element._removeStyle( 'color' ); // Removes 'color' style. + * element._removeStyle( [ 'color', 'border-top' ] ); // Removes both 'color' and 'border-top' styles. + * + * @protected + * @param {Array.|String} property + * @fires module:engine/view/node~Node#change + */ + _removeStyle( property ) { + this._fireChange( 'attributes', this ); + + property = Array.isArray( property ) ? property : [ property ]; + property.forEach( name => this._styles.delete( name ) ); + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/writer.js b/src/view/writer.js index 898954e68..ad206b034 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -202,6 +202,10 @@ export default class Writer { element._setStyle( property, value ); } + removeStyle( property, element ) { + element._removeStyle( property ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. @@ -1120,7 +1124,7 @@ export default class Writer { this.removeClass( Array.from( wrapper.getClassNames() ), toUnwrap ); // Remove all wrapper's styles from unwrapped element. - toUnwrap.removeStyle( ...wrapper.getStyleNames() ); + this.removeStyle( Array.from( wrapper.getStyleNames() ), toUnwrap ); return true; } diff --git a/tests/view/element.js b/tests/view/element.js index 5374a9e40..22b69a511 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -833,10 +833,10 @@ describe( 'Element', () => { } ); } ); - describe( 'removeStyle', () => { + describe( '_removeStyle()', () => { it( 'should remove style', () => { el._setStyle( 'padding-top', '10px' ); - el.removeStyle( 'padding-top' ); + el._removeStyle( 'padding-top' ); expect( el.hasStyle( 'padding-top' ) ).to.be.false; } ); @@ -848,7 +848,7 @@ describe( 'Element', () => { done(); } ); - el.removeStyle( 'color' ); + el._removeStyle( 'color' ); } ); it( 'should remove multiple styles', () => { @@ -857,7 +857,7 @@ describe( 'Element', () => { 'margin-top': '10px', 'color': 'red' } ); - el.removeStyle( 'padding-top', 'margin-top' ); + el._removeStyle( [ 'padding-top', 'margin-top' ] ); expect( el.hasStyle( 'padding-top' ) ).to.be.false; expect( el.hasStyle( 'margin-top' ) ).to.be.false; From 4041eb09517384220467f1904dd014ee0ddb9ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 18:52:04 +0100 Subject: [PATCH 66/89] Using writer to set custom properties to view element. --- src/conversion/downcast-converters.js | 2 +- src/view/editableelement.js | 2 +- src/view/element.js | 23 ++++++++++++----------- src/view/rooteditableelement.js | 2 +- src/view/writer.js | 4 ++++ tests/conversion/downcast-converters.js | 8 ++++---- tests/conversion/downcastdispatcher.js | 4 ++-- tests/view/element.js | 18 +++++++++--------- 8 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index a64bcd526..a152a6d59 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1012,7 +1012,7 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { viewElement.priority = descriptor.priority; } - viewElement.setCustomProperty( 'highlightDescriptorId', descriptor.id ); + viewElement._setCustomProperty( 'highlightDescriptorId', descriptor.id ); return viewElement; } diff --git a/src/view/editableelement.js b/src/view/editableelement.js index 7bdb694eb..736316537 100644 --- a/src/view/editableelement.js +++ b/src/view/editableelement.js @@ -74,7 +74,7 @@ export default class EditableElement extends ContainerElement { throw new CKEditorError( 'view-editableelement-document-already-set: View document is already set.' ); } - this.setCustomProperty( documentSymbol, document ); + this._setCustomProperty( documentSymbol, document ); this.bind( 'isReadOnly' ).to( document ); diff --git a/src/view/element.js b/src/view/element.js index d5f962160..3dcc0488d 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -510,17 +510,6 @@ export default class Element extends Node { return null; } - /** - * Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM, - * so they can be used to add special data to elements. - * - * @param {String|Symbol} key - * @param {*} value - */ - setCustomProperty( key, value ) { - this._customProperties.set( key, value ); - } - /** * Returns the custom property value for the given key. * @@ -722,6 +711,18 @@ export default class Element extends Node { property.forEach( name => this._styles.delete( name ) ); } + /** + * Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM, + * so they can be used to add special data to elements. + * + * @protected + * @param {String|Symbol} key + * @param {*} value + */ + _setCustomProperty( key, value ) { + this._customProperties.set( key, value ); + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/rooteditableelement.js b/src/view/rooteditableelement.js index 438d7d423..20eef430c 100644 --- a/src/view/rooteditableelement.js +++ b/src/view/rooteditableelement.js @@ -53,7 +53,7 @@ export default class RootEditableElement extends EditableElement { } set rootName( rootName ) { - this.setCustomProperty( rootNameSymbol, rootName ); + this._setCustomProperty( rootNameSymbol, rootName ); } /** diff --git a/src/view/writer.js b/src/view/writer.js index ad206b034..4a4abf5de 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -206,6 +206,10 @@ export default class Writer { element._removeStyle( property ); } + setCustomProperty( key, value, element ) { + element._setCustomProperty( key, value ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index 50b37e13f..de29d8839 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -1302,13 +1302,13 @@ describe( 'downcast-converters', () => { dispatcher.on( 'insert:div', insertElement( () => { const viewContainer = new ViewContainerElement( 'div' ); - viewContainer.setCustomProperty( 'addHighlight', ( element, descriptor ) => { + viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor ) => { controller.view.change( writer => { writer.addClass( descriptor.class, element ); } ); } ); - viewContainer.setCustomProperty( 'removeHighlight', element => { + viewContainer._setCustomProperty( 'removeHighlight', element => { controller.view.change( writer => { writer.setAttribute( 'class', '', element ); } ); @@ -1383,12 +1383,12 @@ describe( 'downcast-converters', () => { dispatcher.on( 'addMarker:marker2', highlightElement( () => null ) ); dispatcher.on( 'removeMarker:marker2', removeHighlight( () => null ) ); - viewDiv.setCustomProperty( 'addHighlight', ( element, descriptor ) => { + viewDiv._setCustomProperty( 'addHighlight', ( element, descriptor ) => { expect( descriptor.priority ).to.equal( 10 ); expect( descriptor.id ).to.equal( 'marker:foo-bar-baz' ); } ); - viewDiv.setCustomProperty( 'removeHighlight', ( element, id ) => { + viewDiv._setCustomProperty( 'removeHighlight', ( element, id ) => { expect( id ).to.equal( 'marker:foo-bar-baz' ); } ); diff --git a/tests/conversion/downcastdispatcher.js b/tests/conversion/downcastdispatcher.js index ab9b13645..bd2045ee2 100644 --- a/tests/conversion/downcastdispatcher.js +++ b/tests/conversion/downcastdispatcher.js @@ -385,8 +385,8 @@ describe( 'DowncastDispatcher', () => { const viewFigure = new ViewContainerElement( 'figure', null, viewCaption ); // Create custom highlight handler mock. - viewFigure.setCustomProperty( 'addHighlight', () => { } ); - viewFigure.setCustomProperty( 'removeHighlight', () => { } ); + viewFigure._setCustomProperty( 'addHighlight', () => { } ); + viewFigure._setCustomProperty( 'removeHighlight', () => { } ); // Create mapper mock. dispatcher.conversionApi.mapper = { diff --git a/tests/view/element.js b/tests/view/element.js index 22b69a511..8efb69567 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -198,8 +198,8 @@ describe( 'Element', () => { it( 'should clone custom properties', () => { const el = new Element( 'p' ); const symbol = Symbol( 'custom' ); - el.setCustomProperty( 'foo', 'bar' ); - el.setCustomProperty( symbol, 'baz' ); + el._setCustomProperty( 'foo', 'bar' ); + el._setCustomProperty( symbol, 'baz' ); const cloned = el.clone(); @@ -964,7 +964,7 @@ describe( 'Element', () => { describe( 'custom properties', () => { it( 'should allow to set and get custom properties', () => { const el = new Element( 'p' ); - el.setCustomProperty( 'foo', 'bar' ); + el._setCustomProperty( 'foo', 'bar' ); expect( el.getCustomProperty( 'foo' ) ).to.equal( 'bar' ); } ); @@ -972,7 +972,7 @@ describe( 'Element', () => { it( 'should allow to add symbol property', () => { const el = new Element( 'p' ); const symbol = Symbol( 'custom' ); - el.setCustomProperty( symbol, 'bar' ); + el._setCustomProperty( symbol, 'bar' ); expect( el.getCustomProperty( symbol ) ).to.equal( 'bar' ); } ); @@ -980,8 +980,8 @@ describe( 'Element', () => { it( 'should allow to remove custom property', () => { const el = new Element( 'foo' ); const symbol = Symbol( 'quix' ); - el.setCustomProperty( 'bar', 'baz' ); - el.setCustomProperty( symbol, 'test' ); + el._setCustomProperty( 'bar', 'baz' ); + el._setCustomProperty( symbol, 'test' ); expect( el.getCustomProperty( 'bar' ) ).to.equal( 'baz' ); expect( el.getCustomProperty( symbol ) ).to.equal( 'test' ); @@ -995,9 +995,9 @@ describe( 'Element', () => { it( 'should allow to iterate over custom properties', () => { const el = new Element( 'p' ); - el.setCustomProperty( 'foo', 1 ); - el.setCustomProperty( 'bar', 2 ); - el.setCustomProperty( 'baz', 3 ); + el._setCustomProperty( 'foo', 1 ); + el._setCustomProperty( 'bar', 2 ); + el._setCustomProperty( 'baz', 3 ); const properties = [ ...el.getCustomProperties() ]; From 01ce914803daddf749541968ae777708aee4de9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 18:56:02 +0100 Subject: [PATCH 67/89] Using writer to remove custom properties from view element. --- src/view/element.js | 21 +++++++++++---------- src/view/writer.js | 4 ++++ tests/view/element.js | 4 ++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/view/element.js b/src/view/element.js index 3dcc0488d..210530b74 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -520,16 +520,6 @@ export default class Element extends Node { return this._customProperties.get( key ); } - /** - * Removes the custom property stored under the given key. - * - * @param {String|Symbol} key - * @returns {Boolean} Returns true if property was removed. - */ - removeCustomProperty( key ) { - return this._customProperties.delete( key ); - } - /** * Returns an iterator which iterates over this element's custom properties. * Iterator provides `[ key, value ]` pairs for each stored property. @@ -723,6 +713,17 @@ export default class Element extends Node { this._customProperties.set( key, value ); } + /** + * Removes the custom property stored under the given key. + * + * @protected + * @param {String|Symbol} key + * @returns {Boolean} Returns true if property was removed. + */ + _removeCustomProperty( key ) { + return this._customProperties.delete( key ); + } + /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. * diff --git a/src/view/writer.js b/src/view/writer.js index 4a4abf5de..188b733da 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -210,6 +210,10 @@ export default class Writer { element._setCustomProperty( key, value ); } + removeCustomProperty( key, element ) { + element._removeCustomProperty( key ); + } + /** * Breaks attribute nodes at provided position or at boundaries of provided range. It breaks attribute elements inside * up to a container element. diff --git a/tests/view/element.js b/tests/view/element.js index 8efb69567..b3d995b43 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -986,8 +986,8 @@ describe( 'Element', () => { expect( el.getCustomProperty( 'bar' ) ).to.equal( 'baz' ); expect( el.getCustomProperty( symbol ) ).to.equal( 'test' ); - el.removeCustomProperty( 'bar' ); - el.removeCustomProperty( symbol ); + el._removeCustomProperty( 'bar' ); + el._removeCustomProperty( symbol ); expect( el.getCustomProperty( 'bar' ) ).to.be.undefined; expect( el.getCustomProperty( symbol ) ).to.be.undefined; From cefdba4fa36bfeb4db9b2c578a32d12fc1f1c165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 21:31:58 +0100 Subject: [PATCH 68/89] AttributeElement priority is now protected. EditableElement document is now protected. --- src/controller/editingcontroller.js | 2 +- src/conversion/downcast-converters.js | 2 +- src/dev-utils/model.js | 2 +- src/dev-utils/view.js | 4 ++-- src/view/attributeelement.js | 15 +++++++++++++-- src/view/editableelement.js | 14 +++++++++++++- src/view/writer.js | 16 +++++++++++----- tests/dev-utils/view.js | 4 ++-- tests/view/_utils/createroot.js | 2 +- tests/view/attributeelement.js | 8 ++++---- tests/view/domconverter/domconverter.js | 2 +- tests/view/editableelement.js | 20 ++++++++++---------- tests/view/node.js | 4 ++-- tests/view/position.js | 2 +- tests/view/rooteditableelement.js | 8 ++++---- tests/view/textproxy.js | 4 ++-- tests/view/view/view.js | 2 +- 17 files changed, 70 insertions(+), 41 deletions(-) diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index 56568ef2b..21f0b123d 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -157,7 +157,7 @@ export default class EditingController { const viewRoot = new RootEditableElement( root.name ); viewRoot.rootName = root.rootName; - viewRoot.document = this.view.document; + viewRoot._document = this.view.document; this.mapper.bindElements( root, viewRoot ); return viewRoot; diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index a152a6d59..561cd47e2 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -1009,7 +1009,7 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { } if ( descriptor.priority ) { - viewElement.priority = descriptor.priority; + viewElement._priority = descriptor.priority; } viewElement._setCustomProperty( 'highlightDescriptorId', descriptor.id ); diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 982371699..2500e298d 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -197,7 +197,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) { const viewRoot = new ViewRootEditableElement( 'div' ); // Create a temporary root element in view document. - viewRoot.document = view.document; + viewRoot._document = view.document; viewRoot.rootName = 'main'; viewDocument.roots.add( viewRoot ); diff --git a/src/dev-utils/view.js b/src/dev-utils/view.js index 60bdebacc..bb20f74b4 100644 --- a/src/dev-utils/view.js +++ b/src/dev-utils/view.js @@ -192,7 +192,7 @@ setData._parse = parse; * {@link module:engine/view/attributeelement~AttributeElement attribute elements}. * * const attribute = new AttributeElement( 'b' ); - * attribute.priority = 20; + * attribute._priority = 20; * getData( attribute, null, { showPriority: true } ); // * * @param {module:engine/view/text~Text|module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment} @@ -924,7 +924,7 @@ function _convertElement( viewElement ) { if ( newElement.is( 'attributeElement' ) ) { if ( info.priority !== null ) { - newElement.priority = info.priority; + newElement._priority = info.priority; } } diff --git a/src/view/attributeelement.js b/src/view/attributeelement.js index 8e610d3de..5c0892d94 100644 --- a/src/view/attributeelement.js +++ b/src/view/attributeelement.js @@ -36,9 +36,10 @@ export default class AttributeElement extends Element { * {@link module:engine/view/element~Element#isSimilar similar}. Setting different priorities on similar * nodes may prevent merging, e.g. two `` nodes next each other shouldn't be merged. * + * @protected * @member {Number} */ - this.priority = DEFAULT_PRIORITY; + this._priority = DEFAULT_PRIORITY; /** * Returns block {@link module:engine/view/filler filler} offset or `null` if block filler is not needed. @@ -49,6 +50,16 @@ export default class AttributeElement extends Element { this.getFillerOffset = getFillerOffset; } + /** + * Priority of this element. + * + * @readonly + * @return {Number} + */ + get priority() { + return this._priority; + } + /** * @inheritDoc */ @@ -71,7 +82,7 @@ export default class AttributeElement extends Element { const cloned = super.clone( deep ); // Clone priority too. - cloned.priority = this.priority; + cloned._priority = this._priority; return cloned; } diff --git a/src/view/editableelement.js b/src/view/editableelement.js index 736316537..8c8845953 100644 --- a/src/view/editableelement.js +++ b/src/view/editableelement.js @@ -60,11 +60,23 @@ export default class EditableElement extends ContainerElement { */ } + /** + * Returns document associated with the editable. + * + * @readonly + * @return {module:engine/view/document~Document} + */ get document() { return this.getCustomProperty( documentSymbol ); } - set document( document ) { + /** + * Sets document of this editable element. + * + * @protected + * @param {module:engine/view/document~Document} document + */ + set _document( document ) { if ( this.getCustomProperty( documentSymbol ) ) { /** * View document is already set. It can only be set once. diff --git a/src/view/writer.js b/src/view/writer.js index 188b733da..0981b8617 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -118,8 +118,14 @@ export default class Writer { * @param {Object} [attributes] Elements attributes. * @returns {module:engine/view/attributeelement~AttributeElement} Created element. */ - createAttributeElement( name, attributes ) { - return new AttributeElement( name, attributes ); + createAttributeElement( name, attributes, priority ) { + const attributeElement = new AttributeElement( name, attributes ); + + if ( priority ) { + attributeElement._priority = priority; + } + + return attributeElement; } /** @@ -149,7 +155,7 @@ export default class Writer { */ createEditableElement( name, attributes ) { const editableElement = new EditableElement( name, attributes ); - editableElement.document = this.document; + editableElement._document = this.document; return editableElement; } @@ -983,8 +989,8 @@ export default class Writer { } // Create fake element that will represent position, and will not be merged with other attributes. - const fakePosition = new AttributeElement(); - fakePosition.priority = Number.POSITIVE_INFINITY; + const fakePosition = this.createAttributeElement(); + fakePosition._priority = Number.POSITIVE_INFINITY; fakePosition.isSimilar = () => false; // Insert fake element in position location. diff --git a/tests/dev-utils/view.js b/tests/dev-utils/view.js index 8e2658a06..a9aa5051a 100644 --- a/tests/dev-utils/view.js +++ b/tests/dev-utils/view.js @@ -455,10 +455,10 @@ describe( 'view test utils', () => { it( 'should parse element priority', () => { const parsed1 = parse( '' ); const attribute1 = new AttributeElement( 'b' ); - attribute1.priority = 12; + attribute1._priority = 12; const parsed2 = parse( '' ); const attribute2 = new AttributeElement( 'b' ); - attribute2.priority = 44; + attribute2._priority = 44; parsed1.isSimilar( attribute1 ); expect( parsed1.isSimilar( attribute1 ) ).to.be.true; diff --git a/tests/view/_utils/createroot.js b/tests/view/_utils/createroot.js index cff2d8a4a..c83f0ef8e 100644 --- a/tests/view/_utils/createroot.js +++ b/tests/view/_utils/createroot.js @@ -16,7 +16,7 @@ import RootEditableElement from '../../../src/view/rooteditableelement'; export default function createRoot( doc, name = 'div', rootName = 'main' ) { const root = new RootEditableElement( name ); - root.document = doc; + root._document = doc; root.rootName = rootName; doc.roots.add( root ); diff --git a/tests/view/attributeelement.js b/tests/view/attributeelement.js index e3a4cfe49..43b112d00 100644 --- a/tests/view/attributeelement.js +++ b/tests/view/attributeelement.js @@ -51,7 +51,7 @@ describe( 'AttributeElement', () => { describe( 'clone', () => { it( 'should clone element with priority', () => { const el = new AttributeElement( 'b' ); - el.priority = 7; + el._priority = 7; const clone = el.clone(); @@ -64,17 +64,17 @@ describe( 'AttributeElement', () => { describe( 'isSimilar', () => { it( 'should return true if priorities are the same', () => { const b1 = new AttributeElement( 'b' ); - b1.priority = 7; + b1._priority = 7; const b2 = new AttributeElement( 'b' ); - b2.priority = 7; + b2._priority = 7; expect( b1.isSimilar( b2 ) ).to.be.true; } ); it( 'should return false if priorities are different', () => { const b1 = new AttributeElement( 'b' ); - b1.priority = 7; + b1._priority = 7; const b2 = new AttributeElement( 'b' ); // default priority diff --git a/tests/view/domconverter/domconverter.js b/tests/view/domconverter/domconverter.js index 5f9882c4a..21464c185 100644 --- a/tests/view/domconverter/domconverter.js +++ b/tests/view/domconverter/domconverter.js @@ -40,7 +40,7 @@ describe( 'DomConverter', () => { beforeEach( () => { viewDocument = new ViewDocument(); viewEditable = new ViewEditable( 'div' ); - viewEditable.document = viewDocument; + viewEditable._document = viewDocument; domEditable = document.createElement( 'div' ); domEditableParent = document.createElement( 'div' ); diff --git a/tests/view/editableelement.js b/tests/view/editableelement.js index 1b0e59b97..38c603dfc 100644 --- a/tests/view/editableelement.js +++ b/tests/view/editableelement.js @@ -18,7 +18,7 @@ describe( 'EditableElement', () => { } ); it( 'should allow to set document', () => { - element.document = docMock; + element._document = docMock; expect( element.document ).to.equal( docMock ); } ); @@ -28,16 +28,16 @@ describe( 'EditableElement', () => { } ); it( 'should throw if trying to set document again', () => { - element.document = docMock; + element._document = docMock; const newDoc = createDocumentMock(); expect( () => { - element.document = newDoc; + element._document = newDoc; } ).to.throw( CKEditorError, 'view-editableelement-document-already-set: View document is already set.' ); } ); it( 'should be cloned properly', () => { - element.document = docMock; + element._document = docMock; const newElement = element.clone(); expect( newElement.document ).to.equal( docMock ); @@ -51,16 +51,16 @@ describe( 'EditableElement', () => { docMock = createDocumentMock(); viewMain = new RootEditableElement( 'div' ); - viewMain.document = docMock; + viewMain._document = docMock; viewHeader = new RootEditableElement( 'h1' ); - viewHeader.document = docMock; + viewHeader._document = docMock; viewHeader.rootName = 'header'; } ); it( 'should be observable', () => { const root = new RootEditableElement( 'div' ); - root.document = createDocumentMock(); + root._document = createDocumentMock(); expect( root.isFocused ).to.be.false; @@ -114,7 +114,7 @@ describe( 'EditableElement', () => { describe( 'isReadOnly', () => { it( 'should be observable', () => { const root = new RootEditableElement( 'div' ); - root.document = createDocumentMock(); + root._document = createDocumentMock(); expect( root.isReadOnly ).to.be.false; @@ -131,7 +131,7 @@ describe( 'EditableElement', () => { it( 'should be bound to the document#isReadOnly', () => { const root = new RootEditableElement( 'div' ); - root.document = createDocumentMock(); + root._document = createDocumentMock(); root.document.isReadOnly = false; @@ -147,7 +147,7 @@ describe( 'EditableElement', () => { it( 'should return document', () => { const docMock = createDocumentMock(); const root = new RootEditableElement( 'div' ); - root.document = docMock; + root._document = docMock; expect( root.document ).to.equal( docMock ); } ); diff --git a/tests/view/node.js b/tests/view/node.js index 564e884cb..3f04cd58c 100644 --- a/tests/view/node.js +++ b/tests/view/node.js @@ -222,7 +222,7 @@ describe( 'Node', () => { it( 'should return Document attached to the parent element', () => { const docMock = createDocumentMock(); const parent = new RootEditableElement( 'div' ); - parent.document = docMock; + parent._document = docMock; const child = new Element( 'p' ); child.parent = parent; @@ -248,7 +248,7 @@ describe( 'Node', () => { it( 'should return root element', () => { const parent = new RootEditableElement( 'div' ); - parent.document = createDocumentMock(); + parent._document = createDocumentMock(); const child = new Element( 'p' ); child.parent = parent; diff --git a/tests/view/position.js b/tests/view/position.js index 3eb28fd2b..9427a32be 100644 --- a/tests/view/position.js +++ b/tests/view/position.js @@ -511,7 +511,7 @@ describe( 'Position', () => { const document = new Document(); const p = new Element( 'p' ); const editable = new EditableElement( 'div', null, p ); - editable.document = document; + editable._document = document; const position = new Position( p, 0 ); expect( position.editableElement ).to.equal( editable ); diff --git a/tests/view/rooteditableelement.js b/tests/view/rooteditableelement.js index eef7eb250..6c7767e86 100644 --- a/tests/view/rooteditableelement.js +++ b/tests/view/rooteditableelement.js @@ -13,7 +13,7 @@ describe( 'RootEditableElement', () => { describe( 'constructor()', () => { it( 'should create an element with default root name', () => { const root = new RootEditableElement( 'div' ); - root.document = createDocumentMock(); + root._document = createDocumentMock(); expect( root ).to.be.instanceof( EditableElement ); expect( root ).to.be.instanceof( ContainerElement ); @@ -27,7 +27,7 @@ describe( 'RootEditableElement', () => { it( 'should create an element with custom root name', () => { const root = new RootEditableElement( 'h1' ); - root.document = createDocumentMock(); + root._document = createDocumentMock(); root.rootName = 'header'; expect( root.rootName ).to.equal( 'header' ); @@ -83,12 +83,12 @@ describe( 'RootEditableElement', () => { it( 'should be cloned properly', () => { const root = new RootEditableElement( 'h1' ); - root.document = createDocumentMock(); + root._document = createDocumentMock(); root.rootName = 'header'; const newRoot = root.clone(); - expect( newRoot.document ).to.equal( root.document ); + expect( newRoot._document ).to.equal( root._document ); expect( newRoot.rootName ).to.equal( root.rootName ); } ); } ); diff --git a/tests/view/textproxy.js b/tests/view/textproxy.js index 69212edbd..a638f74f7 100644 --- a/tests/view/textproxy.js +++ b/tests/view/textproxy.js @@ -92,7 +92,7 @@ describe( 'TextProxy', () => { it( 'should return Document attached to the parent element', () => { const docMock = createDocumentMock(); const root = new RootEditableElement( 'div' ); - root.document = docMock; + root._document = docMock; wrapper.parent = root; @@ -109,7 +109,7 @@ describe( 'TextProxy', () => { describe( 'getRoot', () => { it( 'should return root element', () => { const root = new RootEditableElement( 'div' ); - root.document = createDocumentMock(); + root._document = createDocumentMock(); wrapper.parent = root; diff --git a/tests/view/view/view.js b/tests/view/view/view.js index 3620cb0c6..126d532e6 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -533,7 +533,7 @@ describe( 'view', () => { const viewRoot = new RootEditableElement( name ); viewRoot.rootName = rootName; - viewRoot.document = viewDoc; + viewRoot._document = viewDoc; viewDoc.roots.add( viewRoot ); return viewRoot; From d4fe28fa69fd5eb340d3000a5c8a2ec131272d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 21:37:53 +0100 Subject: [PATCH 69/89] View writer.createUIElement() method can now initialize rendering method. --- src/view/writer.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/view/writer.js b/src/view/writer.js index 0981b8617..1693725e1 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -177,15 +177,31 @@ export default class Writer { /** * Creates new {@link module:engine/view/uielement~UIElement}. * - * writer.createUIElement( 'paragraph' ); - * writer.createUIElement( 'paragraph', { 'alignment': 'center' } ); + * writer.createUIElement( 'span' ); + * writer.createUIElement( 'span', { 'alignment': 'center' } ); + * + * Custom render function can be provided as third parameter: + * + * writer.createUIElement( 'span', null, function( domDocument ) { + * const domElement = this.toDomElement( domDocument ); + * domElement.innerHTML = 'this is ui element'; + * + * return domElement; + * } ); * * @param {String} name Name of the element. * @param {Object} [attributes] Elements attributes. + * @param {Function} [renderFunction] Custom render function. * @returns {module:engine/view/uielement~UIElement} Created element. */ - createUIElement( name, attributes ) { - return new UIElement( name, attributes ); + createUIElement( name, attributes, renderFunction ) { + const uiElement = new UIElement( name, attributes ); + + if ( renderFunction ) { + uiElement.render = renderFunction; + } + + return uiElement; } setAttribute( key, value, element ) { From 833b81ff77b46d7ef5be33c8e9861ff11f677169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 21:59:15 +0100 Subject: [PATCH 70/89] Updated view writer's docs. --- src/conversion/downcast-converters.js | 4 +- src/view/writer.js | 79 ++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 561cd47e2..345350d44 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -855,7 +855,7 @@ export function highlightText( highlightDescriptor ) { * Converter function factory. Creates a function which applies the marker's highlight to an element inside the marker's range. * * The converter checks if an element has `addHighlight` function stored as - * {@link module:engine/view/element~Element#setCustomProperty custom property} and, if so, uses it to apply the highlight. + * {@link module:engine/view/element~Element#_setCustomProperty custom property} and, if so, uses it to apply the highlight. * In such case converter will consume all element's children, assuming that they were handled by element itself. * * When `addHighlight` custom property is not present, element is not converted in any special way. @@ -913,7 +913,7 @@ export function highlightElement( highlightDescriptor ) { * highlight descriptor. See {link module:engine/conversion/downcast-converters~highlightDescriptorToAttributeElement}. * * For elements, the converter checks if an element has `removeHighlight` function stored as - * {@link module:engine/view/element~Element#setCustomProperty custom property}. If so, it uses it to remove the highlight. + * {@link module:engine/view/element~Element#_setCustomProperty custom property}. If so, it uses it to remove the highlight. * In such case, children of that element will not be converted. * * When `removeHighlight` is not present, element is not converted in any special way. diff --git a/src/view/writer.js b/src/view/writer.js index 1693725e1..ebaca1991 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -16,6 +16,7 @@ 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 isPlainObject from '@ckeditor/ckeditor5-utils/src/lib/lodash/isPlainObject'; import Text from './text'; import EditableElement from './editableelement'; @@ -204,36 +205,112 @@ export default class Writer { return uiElement; } + /** + * Adds or overwrite element's attribute with a specified key and value. + * + * writer.setAttribute( 'href', 'http://ckeditor.com', linkElement ); + * + * @param {String} key Attribute key. + * @param {String} value Attribute value. + * @param {module:engine/view/element~Element} element + */ setAttribute( key, value, element ) { element._setAttribute( key, value ); } + /** + * Removes attribute from the element. + * + * writer.removeAttribute( 'href', linkElement ); + * + * @param {String} key Attribute key. + * @param {module:engine/view/element~Element} element + */ removeAttribute( key, element ) { element._removeAttribute( key ); } + /** + * Adds specified class to the element. + * + * writer.addClass( 'foo', linkElement ); + * writer.addClass( [ 'foo', 'bar' ], linkElement ); + * + * @param {Array.|String} className + * @param {module:engine/view/element~Element} element + */ addClass( className, element ) { element._addClass( className ); } + /** + * Removes specified class from the element. + * + * writer.removeClass( 'foo', linkElement ); + * writer.removeClass( [ 'foo', 'bar' ], linkElement ); + * + * @param {Array.|String} className + * @param {module:engine/view/element~Element} element + */ removeClass( className, element ) { element._removeClass( className ); } + /** + * Adds style to the element. + * + * writer.setStyle( 'color', 'red', element ); + * writer.setStyle( { + * color: 'red', + * position: 'fixed' + * }, element ); + * + * @param {String|Object} property Property name or object with key - value pairs. + * @param {String} [value] Value to set. This parameter is ignored if object is provided as the first parameter. + * @param {module:engine/view/element~Element} element Element to set styles on. + */ setStyle( property, value, element ) { + if ( isPlainObject( property ) && element === undefined ) { + element = value; + } + element._setStyle( property, value ); } + /** + * Removes specified style from the element. + * + * writer.removeStyle( 'color', element ); // Removes 'color' style. + * writer.removeStyle( [ 'color', 'border-top' ], element ); // Removes both 'color' and 'border-top' styles. + * + * @param {Array.|String} property + * @param {module:engine/view/element~Element} element + */ removeStyle( property, element ) { element._removeStyle( property ); } + /** + * Sets a custom property on element. Unlike attributes, custom properties are not rendered to the DOM, + * so they can be used to add special data to elements. + * + * @param {String|Symbol} key + * @param {*} value + * @param {module:engine/view/element~Element} element + */ setCustomProperty( key, value, element ) { element._setCustomProperty( key, value ); } + /** + * Removes a custom property stored under the given key. + * + * @param {String|Symbol} key + * @param {module:engine/view/element~Element} element + * @returns {Boolean} Returns true if property was removed. + */ removeCustomProperty( key, element ) { - element._removeCustomProperty( key ); + return element._removeCustomProperty( key ); } /** From 3f110deb3c9025b9d661851ba377b3acf412b73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Tue, 13 Feb 2018 23:27:01 +0100 Subject: [PATCH 71/89] Added tests to new methods in view writer. --- tests/view/writer/writer.js | 142 ++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/tests/view/writer/writer.js b/tests/view/writer/writer.js index 14b19a98d..834126cab 100644 --- a/tests/view/writer/writer.js +++ b/tests/view/writer/writer.js @@ -68,6 +68,15 @@ describe( 'Writer', () => { expect( element.name ).to.equal( 'foo' ); assertElementAttributes( element, attributes ); } ); + + it( 'should allow to pass priority', () => { + const element = writer.createAttributeElement( 'foo', attributes, 99 ); + + expect( element.is( 'attributeElement' ) ).to.be.true; + expect( element.name ).to.equal( 'foo' ); + expect( element.priority ).to.equal( 99 ); + assertElementAttributes( element, attributes ); + } ); } ); describe( 'createContainerElement()', () => { @@ -108,6 +117,139 @@ describe( 'Writer', () => { expect( element.name ).to.equal( 'foo' ); assertElementAttributes( element, attributes ); } ); + + it( 'should allow to pass custom rendering method', () => { + const renderFn = function() {}; + const element = writer.createUIElement( 'foo', attributes, renderFn ); + + expect( element.is( 'uiElement' ) ).to.be.true; + expect( element.name ).to.equal( 'foo' ); + expect( element.render ).to.equal( renderFn ); + assertElementAttributes( element, attributes ); + } ); + } ); + + describe( 'setAttribute()', () => { + it( 'should set attribute on given element', () => { + const element = writer.createAttributeElement( 'span' ); + + writer.setAttribute( 'foo', 'bar', element ); + + expect( element.getAttribute( 'foo' ) ).to.equal( 'bar' ); + } ); + } ); + + describe( 'removeAttribute()', () => { + it( 'should remove attribute on given element', () => { + const element = writer.createAttributeElement( 'span', { foo: 'bar' } ); + + writer.removeAttribute( 'foo', element ); + + expect( element.getAttribute( 'foo' ) ).to.be.undefined; + } ); + } ); + + describe( 'addClass()', () => { + it( 'should add class to given element', () => { + const element = writer.createAttributeElement( 'span' ); + + writer.addClass( 'foo', element ); + + expect( element.hasClass( 'foo' ) ).to.be.true; + } ); + + it( 'should add multiple classes to given element', () => { + const element = writer.createAttributeElement( 'span' ); + + writer.addClass( [ 'foo', 'bar' ], element ); + + expect( element.hasClass( 'foo' ) ).to.be.true; + expect( element.hasClass( 'bar' ) ).to.be.true; + } ); + } ); + + describe( 'removeClass()', () => { + it( 'should remove class from given element', () => { + const element = writer.createAttributeElement( 'span', { class: 'foo bar' } ); + + writer.removeClass( 'foo', element ); + + expect( element.hasClass( 'foo' ) ).to.be.false; + expect( element.hasClass( 'bar' ) ).to.be.true; + } ); + + it( 'should remove multiple classes from given element', () => { + const element = writer.createAttributeElement( 'span', { class: 'foo bar' } ); + + writer.removeClass( [ 'foo', 'bar' ], element ); + + expect( element.hasClass( 'foo' ) ).to.be.false; + expect( element.hasClass( 'bar' ) ).to.be.false; + } ); + } ); + + describe( 'addStyle()', () => { + it( 'should add style to given element', () => { + const element = writer.createAttributeElement( 'span' ); + + writer.setStyle( 'foo', 'bar', element ); + + expect( element.getStyle( 'foo' ) ).to.equal( 'bar' ); + } ); + + it( 'should allow to add multiple styles to given element', () => { + const element = writer.createAttributeElement( 'span' ); + + writer.setStyle( { + foo: 'bar', + baz: 'quiz' + }, element ); + + expect( element.getStyle( 'foo' ) ).to.equal( 'bar' ); + expect( element.getStyle( 'baz' ) ).to.equal( 'quiz' ); + } ); + } ); + + describe( 'removeStyle()', () => { + it( 'should remove style from given element', () => { + const element = writer.createAttributeElement( 'span', { style: 'foo:bar;baz:quiz;' } ); + + writer.removeStyle( 'foo', element ); + + expect( element.hasStyle( 'foo' ) ).to.be.false; + expect( element.hasStyle( 'baz' ) ).to.be.true; + } ); + + it( 'should remove multiple styles from given element', () => { + const element = writer.createAttributeElement( 'span', { style: 'foo:bar;baz:quiz;' } ); + + writer.removeStyle( [ 'foo', 'bar' ], element ); + + expect( element.hasStyle( 'foo' ) ).to.be.false; + expect( element.hasStyle( 'baz' ) ).to.be.true; + } ); + } ); + + describe( 'setCustomProperty()', () => { + it( 'should set custom property to given element', () => { + const element = writer.createAttributeElement( 'span' ); + + writer.setCustomProperty( 'foo', 'bar', element ); + + expect( element.getCustomProperty( 'foo' ) ).to.equal( 'bar' ); + } ); + } ); + + describe( 'removeCustomProperty()', () => { + it( 'should remove custom property from given element', () => { + const element = writer.createAttributeElement( 'span' ); + + writer.setCustomProperty( 'foo', 'bar', element ); + expect( element.getCustomProperty( 'foo' ) ).to.equal( 'bar' ); + + writer.removeCustomProperty( 'foo', element ); + expect( element.getCustomProperty( 'foo' ) ).to.be.undefined; + } ); } ); function assertElementAttributes( element, attributes ) { From 5b1a8bd27e847fe51769401d2b3c5847b5a49496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 00:46:24 +0100 Subject: [PATCH 72/89] Updated placeholder to use view writer to manipulate view nodes. --- src/view/placeholder.js | 48 +++++++++++++++++++++++++-------------- src/view/view.js | 8 ++++--- tests/view/placeholder.js | 4 ++-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/view/placeholder.js b/src/view/placeholder.js index 80c5e61bb..319cf5f9b 100644 --- a/src/view/placeholder.js +++ b/src/view/placeholder.js @@ -30,61 +30,67 @@ export function attachPlaceholder( view, element, placeholderText, checkFunction const document = view.document; // Detach placeholder if was used before. - detachPlaceholder( element ); + detachPlaceholder( view, element ); // Single listener per document. if ( !documentPlaceholders.has( document ) ) { documentPlaceholders.set( document, new Map() ); // Attach listener just before rendering and update placeholders. - listener.listenTo( view.renderer, 'render', () => updateAllPlaceholders( document ), { priority: 'highest' } ); + listener.listenTo( view.renderer, 'render', () => updateAllPlaceholders( view ), { priority: 'highest' } ); } // Store text in element's data attribute. // This data attribute is used in CSS class to show the placeholder. - element._setAttribute( 'data-placeholder', placeholderText ); + view.change( writer => { + writer.setAttribute( 'data-placeholder', placeholderText, element ); + } ); // Store information about placeholder. documentPlaceholders.get( document ).set( element, checkFunction ); // Update right away too. - updateSinglePlaceholder( element, checkFunction ); + updateSinglePlaceholder( view, element, checkFunction ); } /** * Removes placeholder functionality from given element. * + * @param {module:engine/view/view~View} view * @param {module:engine/view/element~Element} element */ -export function detachPlaceholder( element ) { +export function detachPlaceholder( view, element ) { const document = element.document; - element._removeClass( 'ck-placeholder' ); - element._removeAttribute( 'data-placeholder' ); - if ( documentPlaceholders.has( document ) ) { documentPlaceholders.get( document ).delete( element ); } + + view.change( writer => { + writer.removeClass( 'ck-placeholder', element ); + writer.removeAttribute( 'data-placeholder', element ); + } ); } // Updates all placeholders of given document. // // @private -// @param {module:engine/view/document~Document} document -function updateAllPlaceholders( document ) { - const placeholders = documentPlaceholders.get( document ); +// @param {module:engine/view/view~View} view +function updateAllPlaceholders( view ) { + const placeholders = documentPlaceholders.get( view.document ); for ( const [ element, checkFunction ] of placeholders ) { - updateSinglePlaceholder( element, checkFunction ); + updateSinglePlaceholder( view, element, checkFunction ); } } // Updates placeholder class of given element. // // @private +// @param {module:engine/view/view~View} view // @param {module:engine/view/element~Element} element // @param {Function} checkFunction -function updateSinglePlaceholder( element, checkFunction ) { +function updateSinglePlaceholder( view, element, checkFunction ) { const document = element.document; // Element was removed from document. @@ -97,7 +103,9 @@ function updateSinglePlaceholder( element, checkFunction ) { // If checkFunction is provided and returns false - remove placeholder. if ( checkFunction && !checkFunction() ) { - element._removeClass( 'ck-placeholder' ); + view.change( writer => { + writer.removeClass( 'ck-placeholder', element ); + } ); return; } @@ -108,15 +116,21 @@ function updateSinglePlaceholder( element, checkFunction ) { // If element is empty and editor is blurred. if ( !document.isFocused && isEmptyish ) { - element._addClass( 'ck-placeholder' ); + view.change( writer => { + writer.addClass( 'ck-placeholder', element ); + } ); return; } // It there are no child elements and selection is not placed inside element. if ( isEmptyish && anchor && anchor.parent !== element ) { - element._addClass( 'ck-placeholder' ); + view.change( writer => { + writer.addClass( 'ck-placeholder', element ); + } ); } else { - element._removeClass( 'ck-placeholder' ); + view.change( writer => { + writer.removeClass( 'ck-placeholder', element ); + } ); } } diff --git a/src/view/view.js b/src/view/view.js index 08828af18..4bfcbae47 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -22,6 +22,7 @@ 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 priorities from '@ckeditor/ckeditor5-utils/src/priorities'; import { injectUiElementHandling } from './uielement'; import { injectQuirksHandling } from './filler'; @@ -352,13 +353,14 @@ export default class View { * @private */ _render() { - this._renderingInProgress = true; + // Lock just before rendering and unlock just after. + // This way other parts of the code can listen to the `render` event and modify the view tree just before rendering. + this.renderer.once( 'render', () => ( this._renderingInProgress = true ), { priority: priorities.get( 'normal' ) + 1 } ); + this.renderer.once( 'render', () => ( this._renderingInProgress = false ), { priority: priorities.get( 'normal' ) - 1 } ); this.disableObservers(); this.renderer.render(); this.enableObservers(); - - this._renderingInProgress = false; } /** diff --git a/tests/view/placeholder.js b/tests/view/placeholder.js index 1ccf28465..c72a64298 100644 --- a/tests/view/placeholder.js +++ b/tests/view/placeholder.js @@ -78,7 +78,7 @@ describe( 'placeholder', () => { attachPlaceholder( view, element, 'foo bar baz', spy ); - sinon.assert.calledOnce( spy ); + sinon.assert.called( spy ); expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; } ); @@ -185,7 +185,7 @@ describe( 'placeholder', () => { expect( element.getAttribute( 'data-placeholder' ) ).to.equal( 'foo bar baz' ); expect( element.hasClass( 'ck-placeholder' ) ).to.be.true; - detachPlaceholder( element ); + detachPlaceholder( view, element ); expect( element.hasAttribute( 'data-placeholder' ) ).to.be.false; expect( element.hasClass( 'ck-placeholder' ) ).to.be.false; From 7d321b74a9ec2270b62c42927edfddc55851efc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 01:08:09 +0100 Subject: [PATCH 73/89] Using writer in highlight manual test. --- tests/manual/highlight.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 92c0619f3..81f819e2d 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -6,8 +6,7 @@ /* global console, window, document */ import ModelRange from '../../src/model/range'; -import ViewContainerElement from '../../src/view/containerelement'; -import ViewText from '../../src/view/text'; +import ViewPosition from '../../src/view/position'; import { upcastElementToElement, @@ -50,8 +49,10 @@ class FancyWidget extends Plugin { downcastElementToElement( { model: 'fancywidget', - view: () => { - const widgetElement = new ViewContainerElement( 'figure', { class: 'fancy-widget' }, new ViewText( 'widget' ) ); + view: ( modelItem, consumable, conversionApi ) => { + const viewWriter = conversionApi.writer; + const widgetElement = viewWriter.createContainerElement( 'figure', { class: 'fancy-widget' } ); + viewWriter.insert( ViewPosition.createAt( widgetElement ), viewWriter.createText( 'widget' ) ); return toWidget( widgetElement ); } From 2b923bb767bcbdde5ec82e9313ce9c63e5de308f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 09:18:53 +0100 Subject: [PATCH 74/89] Removed view.jsdoc file. --- src/view/view.jsdoc | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/view/view.jsdoc diff --git a/src/view/view.jsdoc b/src/view/view.jsdoc deleted file mode 100644 index 547357158..000000000 --- a/src/view/view.jsdoc +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module engine/view/view - */ From a576a0a4ff9e968e632a5665037b7a7828cadd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 14:33:47 +0100 Subject: [PATCH 75/89] Get rid of view.document#change event and rednerer#render events. Created render event on view controller. Updated tests. --- src/view/observer/mutationobserver.js | 2 +- src/view/placeholder.js | 2 +- src/view/renderer.js | 8 -- src/view/view.js | 108 ++++++++++++------- tests/view/observer/focusobserver.js | 8 +- tests/view/renderer.js | 10 -- tests/view/view/view.js | 143 ++++++++++++++++++-------- 7 files changed, 176 insertions(+), 105 deletions(-) diff --git a/src/view/observer/mutationobserver.js b/src/view/observer/mutationobserver.js index f01e0b17d..aa8e1a37e 100644 --- a/src/view/observer/mutationobserver.js +++ b/src/view/observer/mutationobserver.js @@ -59,7 +59,7 @@ export default class MutationObserver extends Observer { * * @member {module:engine/view/renderer~Renderer} */ - this.renderer = view.renderer; + this.renderer = view._renderer; /** * Observed DOM elements. diff --git a/src/view/placeholder.js b/src/view/placeholder.js index 319cf5f9b..a4793098c 100644 --- a/src/view/placeholder.js +++ b/src/view/placeholder.js @@ -37,7 +37,7 @@ export function attachPlaceholder( view, element, placeholderText, checkFunction documentPlaceholders.set( document, new Map() ); // Attach listener just before rendering and update placeholders. - listener.listenTo( view.renderer, 'render', () => updateAllPlaceholders( view ), { priority: 'highest' } ); + listener.listenTo( view, 'render', () => updateAllPlaceholders( view ) ); } // Store text in element's data attribute. diff --git a/src/view/renderer.js b/src/view/renderer.js index 2073d3b17..fe915e44c 100644 --- a/src/view/renderer.js +++ b/src/view/renderer.js @@ -110,7 +110,6 @@ export default class Renderer { * @type {null|HTMLElement} */ this._fakeSelectionContainer = null; - this.decorate( 'render' ); } /** @@ -709,13 +708,6 @@ 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 4bfcbae47..77eeeb79c 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -22,9 +22,9 @@ 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 priorities from '@ckeditor/ckeditor5-utils/src/priorities'; import { injectUiElementHandling } from './uielement'; import { injectQuirksHandling } from './filler'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** * Editor's view controller class. @@ -71,11 +71,11 @@ export default class View { /** * Instance of the {@link module:engine/view/renderer~Renderer renderer}. * - * @readonly + * @protected * @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. @@ -102,12 +102,15 @@ export default class View { this._ongoingChange = false; /** - * Is set to `true` when rendering view to DOM is currently in progress. + * Is set to `true` when rendering view to DOM was started. + * This is used to check whether view document can accept changes in current state. + * From the moment when rendering to DOM is stared view tree is locked to prevent changes that will not be + * reflected in the DOM. * * @private - * @member {Boolean} module:engine/view/view~View#_renderingInProgress + * @member {Boolean} module:engine/view/view~View#_renderingStarted */ - this._renderingInProgress = false; + this._renderingStarted = false; /** * Writer instance used in {@link #change change method) callbacks. @@ -127,6 +130,11 @@ export default class View { // Inject quirks handlers. injectQuirksHandling( this ); injectUiElementHandling( this ); + + // Use 'low` priority so that all listeners on 'normal` priority will be executed before. + this.on( 'render', () => { + this._render(); + }, { priority: 'low' } ); } /** @@ -148,12 +156,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 ); @@ -291,22 +299,14 @@ export default class View { * When the outermost change block is done and rendering to DOM is over it fires * {@link module:engine/view/document~Document#event:change} event. * + * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when + * change block is used after rendering to DOM has started. + * * @param {Function} callback Callback function which may modify the view. */ change( callback ) { - if ( this._renderingInProgress ) { - /** - * Warning displayed when there is an attempt to make changes in the view tree during the rendering process. - * This may cause unexpected behaviour and inconsistency between the DOM and the view. - * - * @error applying-view-changes-on-rendering - */ - log.warn( - 'applying-view-changes-on-rendering: ' + - 'Attempting to make changes in the view during rendering process. ' + - 'This may cause some unexpected behaviour and inconsistency between the DOM and the view.' - ); - } + // Check if change is performed in correct moment. + this._assertRenderingInProgress(); // If other changes are in progress wait with rendering until every ongoing change is over. if ( this._ongoingChange ) { @@ -315,23 +315,29 @@ export default class View { this._ongoingChange = true; callback( this._writer ); - this._render(); + this.fire( 'render' ); this._ongoingChange = false; - - this.document.fire( 'change' ); + this._renderingStarted = false; } } /** * Renders {@link module:engine/view/document~Document view document} to DOM. If any view changes are * currently in progress, rendering will start after all {@link #change change blocks} are processed. + * + * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when + * trying to re-render when rendering to DOM has already started. */ render() { - // Render only if no ongoing changes in progress. If there are some, view document will be rendered after all + // Check if rendering is performed in correct moment. + this._assertRenderingInProgress(); + + // Render only if no ongoing changes are 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(); + this.fire( 'render' ); + this._renderingStarted = false; } } @@ -353,21 +359,49 @@ export default class View { * @private */ _render() { - // Lock just before rendering and unlock just after. - // This way other parts of the code can listen to the `render` event and modify the view tree just before rendering. - this.renderer.once( 'render', () => ( this._renderingInProgress = true ), { priority: priorities.get( 'normal' ) + 1 } ); - this.renderer.once( 'render', () => ( this._renderingInProgress = false ), { priority: priorities.get( 'normal' ) - 1 } ); + this._renderingStarted = true; this.disableObservers(); - this.renderer.render(); + this._renderer.render(); this.enableObservers(); } /** - * Fired after a topmost {@link module:engine/view/view~View#change change block} is finished and DOM rendering has + * Throws `applying-view-changes-on-rendering` error when trying to modify or re-render view tree when rendering is + * already started + * + * @private + */ + _assertRenderingInProgress() { + if ( this._renderingStarted ) { + /** + * There is an attempt to make changes in the view tree after the rendering process + * has started. This may cause unexpected behaviour and inconsistency between the DOM and the view. + * This may be caused by: + * * calling `view.change()` or `view.render()` methods during rendering process, + * * calling `view.change()` or `view.render()` methods in callbacks to + * {module:engine/view/document~Document#event:change view document change event) on `low` priority, after + * rendering is over for current `change` block. + * + * @error applying-view-changes-on-rendering + */ + throw new CKEditorError( + 'applying-view-changes-on-rendering: ' + + 'Attempting to make changes in the view during rendering process. ' + + 'This may cause some unexpected behaviour and inconsistency between the DOM and the view.' + ); + } + } + + /** + * Fired after a topmost {@link module:engine/view/view~View#change change block} is finished and the DOM rendering has * been executed. * - * @event module:engine/view/document~Document#event:change + * Actual rendering is performed on 'low' priority. This means that all listeners on 'normal' and above priorities + * will be executed after changes made to view tree but before rendering to the DOM. Use `low` priority for callbacks that + * should be executed after rendering to the DOM. + * + * @event module:engine/view/view~View#event:render */ } diff --git a/tests/view/observer/focusobserver.js b/tests/view/observer/focusobserver.js index 6a0bfc4c4..06d65bdc1 100644 --- a/tests/view/observer/focusobserver.js +++ b/tests/view/observer/focusobserver.js @@ -170,9 +170,9 @@ describe( 'FocusObserver', () => { view.render(); viewDocument.on( 'selectionChange', selectionChangeSpy ); - view.renderer.on( 'render', renderSpy, { priority: 'low' } ); + view.on( 'render', renderSpy, { priority: 'low' } ); - view.renderer.on( 'render', () => { + view.on( 'render', () => { sinon.assert.callOrder( selectionChangeSpy, renderSpy ); done(); }, { priority: 'low' } ); @@ -192,9 +192,9 @@ describe( 'FocusObserver', () => { const domEditable = domRoot.childNodes[ 0 ]; viewDocument.on( 'selectionChange', selectionChangeSpy ); - view.renderer.on( 'render', renderSpy, { priority: 'low' } ); + view.on( 'render', renderSpy, { priority: 'low' } ); - view.renderer.on( 'render', () => { + view.on( 'render', () => { sinon.assert.notCalled( selectionChangeSpy ); sinon.assert.called( renderSpy ); diff --git a/tests/view/renderer.js b/tests/view/renderer.js index 74d67d7e3..d5ab6a77f 100644 --- a/tests/view/renderer.js +++ b/tests/view/renderer.js @@ -140,16 +140,6 @@ describe( 'Renderer', () => { domRoot.remove(); } ); - it( 'should be decorated', () => { - const spy = sinon.spy(); - - renderer.on( 'render', spy ); - - renderer.render(); - - expect( spy.calledOnce ).to.be.true; - } ); - it( 'should update attributes', () => { viewRoot._setAttribute( 'class', 'foo' ); diff --git a/tests/view/view/view.js b/tests/view/view/view.js index 126d532e6..f23a62414 100644 --- a/tests/view/view/view.js +++ b/tests/view/view/view.js @@ -18,6 +18,7 @@ import ViewElement from '../../../src/view/element'; import ViewPosition from '../../../src/view/position'; import { isBlockFiller, BR_FILLER } from '../../../src/view/filler'; import createElement from '@ckeditor/ckeditor5-utils/src/dom/createelement'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; describe( 'view', () => { const DEFAULT_OBSERVERS_COUNT = 5; @@ -91,7 +92,7 @@ describe( 'view', () => { expect( view.getDomRoot() ).to.equal( domDiv ); expect( view.domConverter.mapViewToDom( viewRoot ) ).to.equal( domDiv ); - expect( view.renderer.markedChildren.has( viewRoot ) ).to.be.true; + expect( view._renderer.markedChildren.has( viewRoot ) ).to.be.true; domDiv.remove(); } ); @@ -106,7 +107,7 @@ describe( 'view', () => { expect( count( view.domRoots ) ).to.equal( 1 ); expect( view.getDomRoot( 'header' ) ).to.equal( domH1 ); expect( view.domConverter.mapViewToDom( viewH1 ) ).to.equal( domH1 ); - expect( view.renderer.markedChildren.has( viewH1 ) ).to.be.true; + expect( view._renderer.markedChildren.has( viewH1 ) ).to.be.true; } ); it( 'should call observe on each observer', () => { @@ -115,7 +116,7 @@ describe( 'view', () => { view = new View(); viewDocument = view.document; - view.renderer.render = sinon.spy(); + view._renderer.render = sinon.spy(); const domDiv1 = document.createElement( 'div' ); domDiv1.setAttribute( 'id', 'editor' ); @@ -141,7 +142,7 @@ describe( 'view', () => { view = new View(); viewDocument = view.document; - view.renderer.render = sinon.spy(); + view._renderer.render = sinon.spy(); } ); afterEach( () => { @@ -197,7 +198,7 @@ describe( 'view', () => { view.render(); sinon.assert.calledOnce( observerMock.disable ); - sinon.assert.calledOnce( view.renderer.render ); + sinon.assert.calledOnce( view._renderer.render ); sinon.assert.calledTwice( observerMock.enable ); } ); @@ -358,19 +359,19 @@ describe( 'view', () => { describe( 'isFocused', () => { it( 'should change renderer.isFocused too', () => { expect( viewDocument.isFocused ).to.equal( false ); - expect( view.renderer.isFocused ).to.equal( false ); + expect( view._renderer.isFocused ).to.equal( false ); viewDocument.isFocused = true; expect( viewDocument.isFocused ).to.equal( true ); - expect( view.renderer.isFocused ).to.equal( true ); + expect( view._renderer.isFocused ).to.equal( true ); } ); } ); describe( 'render()', () => { it( 'disable observers, renders and enable observers', () => { const observerMock = view.addObserver( ObserverMock ); - const renderStub = sinon.stub( view.renderer, 'render' ); + const renderStub = sinon.stub( view._renderer, 'render' ); view.render(); @@ -443,22 +444,26 @@ describe( 'view', () => { } ); describe( 'change()', () => { - it( 'should call render and fire event after the change', () => { - const renderSpy = sinon.spy(); - const changeSpy = sinon.spy(); - view.renderer.on( 'render', renderSpy ); - view.document.on( 'change', changeSpy ); + it( 'should fire render event and it should trigger rendering on low priority', () => { + const renderSpy = sinon.spy( view._renderer, 'render' ); + const beforeSpy = sinon.spy(); + const afterSpy = sinon.spy(); + + view.on( 'render', beforeSpy ); + view.on( 'render', afterSpy, { priority: 'low' } ); view.change( () => {} ); - sinon.assert.callOrder( renderSpy, changeSpy ); + sinon.assert.callOrder( beforeSpy, renderSpy, afterSpy ); } ); - it( 'should render and fire change event once for nested change blocks', () => { - const renderSpy = sinon.spy(); - const changeSpy = sinon.spy(); - view.renderer.on( 'render', renderSpy ); - view.document.on( 'change', changeSpy ); + it( 'should fire render event once for nested change blocks', () => { + const renderSpy = sinon.spy( view._renderer, 'render' ); + const beforeSpy = sinon.spy(); + const afterSpy = sinon.spy(); + + view.on( 'render', beforeSpy ); + view.on( 'render', afterSpy, { priority: 'low' } ); view.change( () => { view.change( () => {} ); @@ -469,16 +474,19 @@ describe( 'view', () => { view.change( () => {} ); } ); + sinon.assert.calledOnce( beforeSpy ); sinon.assert.calledOnce( renderSpy ); - sinon.assert.calledOnce( changeSpy ); - sinon.assert.callOrder( renderSpy, changeSpy ); + sinon.assert.calledOnce( afterSpy ); + sinon.assert.callOrder( beforeSpy, renderSpy, afterSpy ); } ); - it( 'should render and fire change event once even if render is called during the change', () => { - const renderSpy = sinon.spy(); - const changeSpy = sinon.spy(); - view.renderer.on( 'render', renderSpy ); - view.document.on( 'change', changeSpy ); + it( 'should fire render event once even if render is called during the change', () => { + const renderSpy = sinon.spy( view._renderer, 'render' ); + const beforeSpy = sinon.spy(); + const afterSpy = sinon.spy(); + + view.on( 'render', beforeSpy ); + view.on( 'render', afterSpy, { priority: 'low' } ); view.change( () => { view.render(); @@ -488,43 +496,90 @@ describe( 'view', () => { view.render(); } ); + sinon.assert.calledOnce( beforeSpy ); sinon.assert.calledOnce( renderSpy ); - sinon.assert.calledOnce( changeSpy ); - sinon.assert.callOrder( renderSpy, changeSpy ); + sinon.assert.calledOnce( afterSpy ); + sinon.assert.callOrder( beforeSpy, renderSpy, afterSpy ); } ); - it( 'should log warning when someone tries to change view during rendering', () => { + it( 'should throw when someone tries to change view during rendering', () => { const domDiv = document.createElement( 'div' ); const viewRoot = createViewRoot( viewDocument, 'div', 'main' ); - sinon.stub( log, 'warn' ); + let renderingCalled = false; view.attachDomRoot( domDiv ); view.change( writer => { const p = writer.createContainerElement( 'p' ); - const ui = writer.createUIElement( 'span' ); - - // This UIElement will try to modify view tree during rendering. - ui.render = function( domDocument ) { + const ui = writer.createUIElement( 'span', null, function( domDocument ) { const element = this.toDomElement( domDocument ); - view.change( () => {} ); + expect( () => view.change( () => {} ) ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ ); + renderingCalled = true; return element; - }; - + } ); writer.insert( ViewPosition.createAt( p ), ui ); writer.insert( ViewPosition.createAt( viewRoot ), p ); } ); - sinon.assert.calledOnce( log.warn ); - sinon.assert.calledWithExactly( log.warn, - 'applying-view-changes-on-rendering: ' + - 'Attempting to make changes in the view during rendering process. ' + - 'This may cause some unexpected behaviour and inconsistency between the DOM and the view.' - ); + expect( renderingCalled ).to.be.true; + domDiv.remove(); + } ); + + it( 'should throw when someone tries to call render() during rendering', () => { + const domDiv = document.createElement( 'div' ); + const viewRoot = createViewRoot( viewDocument, 'div', 'main' ); + let renderingCalled = false; + view.attachDomRoot( domDiv ); + + view.change( writer => { + const p = writer.createContainerElement( 'p' ); + const ui = writer.createUIElement( 'span', null, function( domDocument ) { + const element = this.toDomElement( domDocument ); + + expect( () => view.render() ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ ); + renderingCalled = true; + + return element; + } ); + writer.insert( ViewPosition.createAt( p ), ui ); + writer.insert( ViewPosition.createAt( viewRoot ), p ); + } ); + expect( renderingCalled ).to.be.true; domDiv.remove(); - log.warn.restore(); + } ); + + it( 'should throw when someone tries to call change() after rendering is finished but still in change block', () => { + view.on( 'render', () => { + expect( () => view.change( () => {} ) ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ ); + }, { priority: 'low' } ); + + view.change( () => {} ); + } ); + + it( 'should throw when someone tries to call render() after rendering is finished but still in change block', () => { + view.on( 'render', () => { + expect( () => view.render() ).to.throw( CKEditorError, /^applying-view-changes-on-rendering/ ); + }, { priority: 'low' } ); + + view.change( () => {} ); + } ); + + it( 'should NOT throw when someone tries to call change() before rendering', () => { + view.on( 'render', () => { + expect( () => view.change( () => {} ) ).not.to.throw( CKEditorError, /^applying-view-changes-on-rendering/ ); + }, { priority: 'normal' } ); + + view.change( () => {} ); + } ); + + it( 'should NOT throw when someone tries to call render() before rendering', () => { + view.on( 'render', () => { + expect( () => view.render() ).not.to.throw( CKEditorError, /^applying-view-changes-on-rendering/ ); + }, { priority: 'normal' } ); + + view.change( () => {} ); } ); } ); } ); From 6242b7769dc2e3027cc87d038332c5191a616d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 15:00:36 +0100 Subject: [PATCH 76/89] Added comment do DataController why we use view writer without change() block. --- src/controller/datacontroller.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index f2386760b..95f01a665 100644 --- a/src/controller/datacontroller.js +++ b/src/controller/datacontroller.js @@ -145,6 +145,9 @@ export default class DataController { const modelRange = ModelRange.createIn( modelElementOrFragment ); const viewDocumentFragment = new ViewDocumentFragment(); + + // Create separate ViewWriter just for data conversion purposes. + // We have no view controller and rendering do DOM in DataController so view.change() block is not used here. const viewWriter = new ViewWriter( new ViewDocument() ); this.mapper.bindElements( modelElementOrFragment, viewDocumentFragment ); From d4a8844bc877d9134ada7e192777c0ec5874292d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 15:15:29 +0100 Subject: [PATCH 77/89] Some docs changes in view writer. --- src/view/writer.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/view/writer.js b/src/view/writer.js index ebaca1991..32e48fc6b 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -112,8 +112,8 @@ export default class Writer { /** * Creates new {@link module:engine/view/attributeelement~AttributeElement}. * - * writer.createAttributeElement( 'paragraph' ); - * writer.createAttributeElement( 'paragraph', { 'alignment': 'center' } ); + * writer.createAttributeElement( 'strong' ); + * writer.createAttributeElement( 'strong', { 'alignment': 'center' } ); * * @param {String} name Name of the element. * @param {Object} [attributes] Elements attributes. @@ -146,8 +146,8 @@ export default class Writer { /** * Creates new {@link module:engine/view/editableelement~EditableElement}. * - * writer.createEditableElement( document, 'paragraph' ); - * writer.createEditableElement( document, 'paragraph', { 'alignment': 'center' } ); + * writer.createEditableElement( document, 'div' ); + * writer.createEditableElement( document, 'div', { 'alignment': 'center' } ); * * @param {module:engine/view/document~Document} document View document. * @param {String} name Name of the element. @@ -164,8 +164,8 @@ export default class Writer { /** * Creates new {@link module:engine/view/emptyelement~EmptyElement}. * - * writer.createEmptyElement( 'paragraph' ); - * writer.createEmptyElement( 'paragraph', { 'alignment': 'center' } ); + * writer.createEmptyElement( 'img' ); + * writer.createEmptyElement( 'img', { 'alignment': 'center' } ); * * @param {String} name Name of the element. * @param {Object} [attributes] Elements attributes. From ce0cca448b9993bd89836a11f8644f67c846d662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 17:21:54 +0100 Subject: [PATCH 78/89] Moved model.change() block out of upcast dispatcher. --- src/controller/datacontroller.js | 6 +- src/conversion/upcastdispatcher.js | 86 ++++++++++++-------------- src/dev-utils/model.js | 7 ++- tests/conversion/two-way-converters.js | 15 +++-- tests/conversion/upcast-converters.js | 39 ++++++------ tests/conversion/upcastdispatcher.js | 54 ++++++++-------- 6 files changed, 101 insertions(+), 106 deletions(-) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index 95f01a665..4c8942f30 100644 --- a/src/controller/datacontroller.js +++ b/src/controller/datacontroller.js @@ -87,7 +87,7 @@ export default class DataController { * @readonly * @member {module:engine/conversion/upcastdispatcher~UpcastDispatcher} */ - this.upcastDispatcher = new UpcastDispatcher( this.model, { + this.upcastDispatcher = new UpcastDispatcher( { schema: model.schema } ); @@ -227,7 +227,9 @@ export default class DataController { * @returns {module:engine/model/documentfragment~DocumentFragment} Output document fragment. */ toModel( viewElementOrFragment, context = '$root' ) { - return this.upcastDispatcher.convert( viewElementOrFragment, context ); + return this.model.change( writer => { + return this.upcastDispatcher.convert( viewElementOrFragment, writer, context ); + } ); } /** diff --git a/src/conversion/upcastdispatcher.js b/src/conversion/upcastdispatcher.js index 372e05e97..8815deaec 100644 --- a/src/conversion/upcastdispatcher.js +++ b/src/conversion/upcastdispatcher.js @@ -97,19 +97,10 @@ export default class UpcastDispatcher { * Creates a `UpcastDispatcher` that operates using passed API. * * @see module:engine/conversion/upcastdispatcher~ViewConversionApi - * @param {module:engine/model/model~Model} model Data model. * @param {Object} [conversionApi] Additional properties for interface that will be passed to events fired * by `UpcastDispatcher`. */ - constructor( model, conversionApi = {} ) { - /** - * Data model. - * - * @private - * @type {module:engine/model/model~Model} - */ - this._model = model; - + constructor( conversionApi = {} ) { /** * List of elements that will be checked after conversion process and if element in the list will be empty it * will be removed from conversion result. @@ -153,62 +144,61 @@ export default class UpcastDispatcher { * @fires documentFragment * @param {module:engine/view/documentfragment~DocumentFragment|module:engine/view/element~Element} viewItem * Part of the view to be converted. + * @param {module:engine/model/writer~Writer} writer Instance of model writer. * @param {module:engine/model/schema~SchemaContextDefinition} [context=['$root']] Elements will be converted according to this context. * @returns {module:engine/model/documentfragment~DocumentFragment} Model data that is a result of the conversion process * wrapped in `DocumentFragment`. Converted marker elements will be set as that document fragment's * {@link module:engine/model/documentfragment~DocumentFragment#markers static markers map}. */ - convert( viewItem, context = [ '$root' ] ) { - return this._model.change( writer => { - this.fire( 'viewCleanup', viewItem ); + convert( viewItem, writer, context = [ '$root' ] ) { + this.fire( 'viewCleanup', viewItem ); - // Create context tree and set position in the top element. - // Items will be converted according to this position. - this._modelCursor = createContextTree( context, writer ); + // Create context tree and set position in the top element. + // Items will be converted according to this position. + this._modelCursor = createContextTree( context, writer ); - // Store writer in conversion as a conversion API - // to be sure that conversion process will use the same batch. - this.conversionApi.writer = writer; + // Store writer in conversion as a conversion API + // to be sure that conversion process will use the same batch. + this.conversionApi.writer = writer; - // Create consumable values list for conversion process. - this.conversionApi.consumable = ViewConsumable.createFrom( viewItem ); + // Create consumable values list for conversion process. + this.conversionApi.consumable = ViewConsumable.createFrom( viewItem ); - // Custom data stored by converter for conversion process. - this.conversionApi.store = {}; + // Custom data stored by converter for conversion process. + this.conversionApi.store = {}; - // Do the conversion. - const { modelRange } = this._convertItem( viewItem, this._modelCursor ); + // Do the conversion. + const { modelRange } = this._convertItem( viewItem, this._modelCursor ); - // Conversion result is always a document fragment so let's create this fragment. - const documentFragment = writer.createDocumentFragment(); + // Conversion result is always a document fragment so let's create this fragment. + const documentFragment = writer.createDocumentFragment(); - // When there is a conversion result. - if ( modelRange ) { - // Remove all empty elements that was added to #_removeIfEmpty list. - this._removeEmptyElements(); - - // Move all items that was converted to context tree to document fragment. - for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) { - writer.append( item, documentFragment ); - } + // When there is a conversion result. + if ( modelRange ) { + // Remove all empty elements that was added to #_removeIfEmpty list. + this._removeEmptyElements(); - // Extract temporary markers elements from model and set as static markers collection. - documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer ); + // Move all items that was converted to context tree to document fragment. + for ( const item of Array.from( this._modelCursor.parent.getChildren() ) ) { + writer.append( item, documentFragment ); } - // Clear context position. - this._modelCursor = null; + // Extract temporary markers elements from model and set as static markers collection. + documentFragment.markers = extractMarkersFromModelFragment( documentFragment, writer ); + } + + // Clear context position. + this._modelCursor = null; - // Clear split elements. - this._removeIfEmpty.clear(); + // Clear split elements. + this._removeIfEmpty.clear(); - // Clear conversion API. - this.conversionApi.writer = null; - this.conversionApi.store = null; + // Clear conversion API. + this.conversionApi.writer = null; + this.conversionApi.store = null; - // Return fragment as conversion result. - return documentFragment; - } ); + // Return fragment as conversion result. + return documentFragment; } /** diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 2500e298d..4321128ab 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -285,7 +285,8 @@ export function parse( data, schema, options = {} ) { } // Set up upcast dispatcher. - const upcastDispatcher = new UpcastDispatcher( new Model(), { schema, mapper } ); + const modelController = new Model(); + const upcastDispatcher = new UpcastDispatcher( { schema, mapper } ); upcastDispatcher.on( 'documentFragment', convertToModelFragment() ); upcastDispatcher.on( 'element:model-text-with-attributes', convertToModelText( true ) ); @@ -295,7 +296,9 @@ export function parse( data, schema, options = {} ) { upcastDispatcher.isDebug = true; // Convert view to model. - let model = upcastDispatcher.convert( viewDocumentFragment.root, options.context || '$root' ); + let model = modelController.change( + writer => upcastDispatcher.convert( viewDocumentFragment.root, writer, options.context || '$root' ) + ); mapper.bindElements( model, viewDocumentFragment.root ); diff --git a/tests/conversion/two-way-converters.js b/tests/conversion/two-way-converters.js index 135f2462c..b23a3cf3b 100644 --- a/tests/conversion/two-way-converters.js +++ b/tests/conversion/two-way-converters.js @@ -21,7 +21,7 @@ import { stringify as viewStringify, parse as viewParse } from '../../src/dev-ut import { stringify as modelStringify } from '../../src/dev-utils/model'; describe( 'two-way-converters', () => { - let viewDispatcher, model, schema, conversion, modelRoot, viewRoot; + let upcastDispatcher, model, schema, conversion, modelRoot, viewRoot; beforeEach( () => { model = new Model(); @@ -45,13 +45,13 @@ describe( 'two-way-converters', () => { inheritAllFrom: '$block' } ); - viewDispatcher = new UpcastDispatcher( model, { schema } ); - viewDispatcher.on( 'text', convertText() ); - viewDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } ); - viewDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); + upcastDispatcher = new UpcastDispatcher( { schema } ); + upcastDispatcher.on( 'text', convertText() ); + upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } ); + upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); - conversion.register( 'upcast', [ viewDispatcher ] ); + conversion.register( 'upcast', [ upcastDispatcher ] ); conversion.register( 'downcast', [ controller.downcastDispatcher ] ); } ); @@ -527,9 +527,8 @@ describe( 'two-way-converters', () => { function loadData( input ) { const parsedView = viewParse( input ); - const convertedModel = viewDispatcher.convert( parsedView ); - model.change( writer => { + const convertedModel = upcastDispatcher.convert( parsedView, writer ); writer.remove( ModelRange.createFromParentsAndOffsets( modelRoot, 0, modelRoot, modelRoot.maxOffset ) ); writer.insert( convertedModel, modelRoot, 0 ); } ); diff --git a/tests/conversion/upcast-converters.js b/tests/conversion/upcast-converters.js index 77ad33bc2..d54147ed7 100644 --- a/tests/conversion/upcast-converters.js +++ b/tests/conversion/upcast-converters.js @@ -27,7 +27,7 @@ import { import { stringify } from '../../src/dev-utils/model'; describe( 'upcast-helpers', () => { - let dispatcher, model, schema, conversion; + let upcastDispatcher, model, schema, conversion; beforeEach( () => { model = new Model(); @@ -50,13 +50,13 @@ describe( 'upcast-helpers', () => { allowAttributes: [ 'bold' ] } ); - dispatcher = new UpcastDispatcher( model, { schema } ); - dispatcher.on( 'text', convertText() ); - dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } ); - dispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); + upcastDispatcher = new UpcastDispatcher( { schema } ); + upcastDispatcher.on( 'text', convertText() ); + upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } ); + upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } ); conversion = new Conversion(); - conversion.register( 'upcast', [ dispatcher ] ); + conversion.register( 'upcast', [ upcastDispatcher ] ); } ); describe( 'upcastElementToElement', () => { @@ -600,18 +600,18 @@ describe( 'upcast-helpers', () => { } ); function expectResult( viewToConvert, modelString, marker ) { - const model = dispatcher.convert( viewToConvert ); + const conversionResult = model.change( writer => upcastDispatcher.convert( viewToConvert, writer ) ); if ( marker ) { - expect( model.markers.has( marker.name ) ).to.be.true; + expect( conversionResult.markers.has( marker.name ) ).to.be.true; - const convertedMarker = model.markers.get( marker.name ); + const convertedMarker = conversionResult.markers.get( marker.name ); expect( convertedMarker.start.path ).to.deep.equal( marker.start ); expect( convertedMarker.end.path ).to.deep.equal( marker.end ); } - expect( stringify( model ) ).to.equal( modelString ); + expect( stringify( conversionResult ) ).to.equal( modelString ); } } ); @@ -627,7 +627,7 @@ describe( 'upcast-converters', () => { context = [ '$root' ]; - dispatcher = new UpcastDispatcher( model, { schema } ); + dispatcher = new UpcastDispatcher( { schema } ); } ); describe( 'convertText()', () => { @@ -636,7 +636,7 @@ describe( 'upcast-converters', () => { dispatcher.on( 'text', convertText() ); - const conversionResult = dispatcher.convert( viewText ); + const conversionResult = model.change( writer => dispatcher.convert( viewText, writer ) ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -658,7 +658,7 @@ describe( 'upcast-converters', () => { } } ); - const conversionResult = dispatcher.convert( viewText, context ); + const conversionResult = model.change( writer => dispatcher.convert( viewText, writer, context ) ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -674,13 +674,12 @@ describe( 'upcast-converters', () => { const viewText = new ViewText( 'foobar' ); dispatcher.on( 'text', convertText() ); - - let conversionResult = dispatcher.convert( viewText, context ); + let conversionResult = model.change( writer => dispatcher.convert( viewText, writer, context ) ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 0 ); - conversionResult = dispatcher.convert( viewText, [ '$block' ] ); + conversionResult = model.change( writer => dispatcher.convert( viewText, writer, [ '$block' ] ) ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.childCount ).to.equal( 1 ); @@ -693,7 +692,7 @@ describe( 'upcast-converters', () => { dispatcher.on( 'text', convertText() ); - const conversionResult = dispatcher.convert( viewText, context ); + const conversionResult = model.change( writer => dispatcher.convert( viewText, writer, context ) ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelText ); @@ -714,7 +713,7 @@ describe( 'upcast-converters', () => { dispatcher.on( 'element', convertToModelFragment() ); dispatcher.on( 'documentFragment', convertToModelFragment() ); - const conversionResult = dispatcher.convert( viewFragment, context ); + const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer, context ) ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.maxOffset ).to.equal( 6 ); @@ -741,7 +740,7 @@ describe( 'upcast-converters', () => { } } ); - const conversionResult = dispatcher.convert( viewP, context ); + const conversionResult = model.change( writer => dispatcher.convert( viewP, writer, context ) ); expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); expect( conversionResult.getChild( 0 ) ).to.be.instanceof( ModelElement ); @@ -775,7 +774,7 @@ describe( 'upcast-converters', () => { spy(); } ); - dispatcher.convert( view ); + model.change( writer => dispatcher.convert( view, writer ) ); sinon.assert.calledTwice( spy ); } ); diff --git a/tests/conversion/upcastdispatcher.js b/tests/conversion/upcastdispatcher.js index 4f1375097..278077ef1 100644 --- a/tests/conversion/upcastdispatcher.js +++ b/tests/conversion/upcastdispatcher.js @@ -30,7 +30,7 @@ describe( 'UpcastDispatcher', () => { describe( 'constructor()', () => { it( 'should create UpcastDispatcher with passed api', () => { const apiObj = {}; - const dispatcher = new UpcastDispatcher( model, { apiObj } ); + const dispatcher = new UpcastDispatcher( { apiObj } ); expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj ); expect( dispatcher.conversionApi ).to.have.property( 'convertItem' ).that.is.instanceof( Function ); @@ -39,7 +39,7 @@ describe( 'UpcastDispatcher', () => { } ); it( 'should have properties', () => { - const dispatcher = new UpcastDispatcher( model ); + const dispatcher = new UpcastDispatcher(); expect( dispatcher._removeIfEmpty ).to.instanceof( Set ); } ); @@ -49,7 +49,7 @@ describe( 'UpcastDispatcher', () => { let dispatcher; beforeEach( () => { - dispatcher = new UpcastDispatcher( model ); + dispatcher = new UpcastDispatcher(); } ); it( 'should create api for current conversion process', () => { @@ -100,7 +100,7 @@ describe( 'UpcastDispatcher', () => { spy(); } ); - dispatcher.convert( viewElement ); + model.change( writer => dispatcher.convert( viewElement, writer ) ); // To be sure that both converters was called. sinon.assert.calledTwice( spy ); @@ -115,7 +115,7 @@ describe( 'UpcastDispatcher', () => { sinon.spy( dispatcher, 'fire' ); const viewP = new ViewContainerElement( 'p' ); - dispatcher.convert( viewP ); + model.change( writer => dispatcher.convert( viewP, writer ) ); expect( dispatcher.fire.calledWith( 'viewCleanup', viewP ) ).to.be.true; } ); @@ -127,9 +127,11 @@ describe( 'UpcastDispatcher', () => { sinon.spy( dispatcher, 'fire' ); - dispatcher.convert( viewText ); - dispatcher.convert( viewElement ); - dispatcher.convert( viewFragment ); + model.change( writer => { + dispatcher.convert( viewText, writer ); + dispatcher.convert( viewElement, writer ); + dispatcher.convert( viewFragment, writer ); + } ); expect( dispatcher.fire.calledWith( 'text' ) ).to.be.true; expect( dispatcher.fire.calledWith( 'element:p' ) ).to.be.true; @@ -163,7 +165,7 @@ describe( 'UpcastDispatcher', () => { data.modelRange = ModelRange.createOn( text ); } ); - const conversionResult = dispatcher.convert( viewText ); + const conversionResult = model.change( writer => dispatcher.convert( viewText, writer ) ); // Check conversion result. // Result should be wrapped in document fragment. @@ -201,7 +203,7 @@ describe( 'UpcastDispatcher', () => { } ); // Use `additionalData` parameter to check if it was passed to the event. - const conversionResult = dispatcher.convert( viewElement ); + const conversionResult = model.change( writer => dispatcher.convert( viewElement, writer ) ); // Check conversion result. // Result should be wrapped in document fragment. @@ -237,7 +239,7 @@ describe( 'UpcastDispatcher', () => { data.modelRange = ModelRange.createOn( text ); } ); - const conversionResult = dispatcher.convert( viewFragment ); + const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer ) ); // Check conversion result. expect( conversionResult ).to.be.instanceof( ModelDocumentFragment ); @@ -288,7 +290,7 @@ describe( 'UpcastDispatcher', () => { spy(); } ); - const result = dispatcher.convert( viewElement ); + const result = model.change( writer => dispatcher.convert( viewElement, writer ) ); // Empty split elements should be removed and we should have the following result: // [

]

foo

@@ -322,7 +324,7 @@ describe( 'UpcastDispatcher', () => { data.modelRange = ModelRange.createIn( data.modelCursor.parent ); } ); - const conversionResult = dispatcher.convert( viewFragment ); + const conversionResult = model.change( writer => dispatcher.convert( viewFragment, writer ) ); expect( conversionResult.markers.size ).to.equal( 2 ); @@ -335,7 +337,7 @@ describe( 'UpcastDispatcher', () => { } ); it( 'should convert according to given context', () => { - dispatcher = new UpcastDispatcher( model, { schema: model.schema } ); + dispatcher = new UpcastDispatcher( { schema: model.schema } ); const spy = sinon.spy(); const viewElement = new ViewContainerElement( 'third' ); @@ -358,12 +360,12 @@ describe( 'UpcastDispatcher', () => { } ); // Default context $root. - dispatcher.convert( viewElement ); + model.change( writer => dispatcher.convert( viewElement, writer ) ); sinon.assert.calledOnce( spy ); expect( checkChildResult ).to.false; // SchemaDefinition as context. - dispatcher.convert( viewElement, [ 'first' ] ); + model.change( writer => dispatcher.convert( viewElement, writer, [ 'first' ] ) ); sinon.assert.calledTwice( spy ); expect( checkChildResult ).to.false; @@ -374,7 +376,7 @@ describe( 'UpcastDispatcher', () => { ] ) ] ); - dispatcher.convert( viewElement, new ModelPosition( fragment, [ 0, 0, 0 ] ) ); + model.change( writer => dispatcher.convert( viewElement, writer, new ModelPosition( fragment, [ 0, 0, 0 ] ) ) ); sinon.assert.calledThrice( spy ); expect( checkChildResult ).to.true; } ); @@ -397,7 +399,7 @@ describe( 'UpcastDispatcher', () => { // Put nodes to documentFragment, this will mock root element and makes possible to create range on them. rootMock = new ModelDocumentFragment( [ modelP, modelText ] ); - dispatcher = new UpcastDispatcher( model, { schema: model.schema } ); + dispatcher = new UpcastDispatcher( { schema: model.schema } ); dispatcher.on( 'element:p', ( evt, data ) => { spyP(); @@ -456,7 +458,7 @@ describe( 'UpcastDispatcher', () => { expect( textResult.modelCursor.path ).to.deep.equal( [ 7 ] ); } ); - dispatcher.convert( new ViewDocumentFragment() ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) ); expect( spy.calledOnce ).to.be.true; expect( spyP.calledOnce ).to.be.true; @@ -471,7 +473,7 @@ describe( 'UpcastDispatcher', () => { expect( conversionApi.convertItem( viewNull, data.modelCursor ).modelRange ).to.equal( null ); } ); - dispatcher.convert( new ViewDocumentFragment() ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) ); expect( spy.calledOnce ).to.be.true; expect( spyNull.calledOnce ).to.be.true; @@ -485,7 +487,7 @@ describe( 'UpcastDispatcher', () => { } ); expect( () => { - dispatcher.convert( new ViewDocumentFragment() ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) ); } ).to.throw( CKEditorError, /^view-conversion-dispatcher-incorrect-result/ ); expect( spy.calledOnce ).to.be.true; @@ -513,7 +515,7 @@ describe( 'UpcastDispatcher', () => { expect( result.modelCursor.path ).to.deep.equal( [ 7 ] ); } ); - dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ) ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment( [ viewP, viewText ] ), writer ) ); expect( spy.calledOnce ).to.be.true; expect( spyP.calledOnce ).to.be.true; @@ -546,7 +548,7 @@ describe( 'UpcastDispatcher', () => { spy(); } ); - dispatcher.convert( new ViewDocumentFragment() ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) ); sinon.assert.calledOnce( spy ); } ); @@ -583,7 +585,7 @@ describe( 'UpcastDispatcher', () => { spy(); } ); - dispatcher.convert( new ViewDocumentFragment() ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) ); sinon.assert.calledOnce( spy ); } ); @@ -603,7 +605,7 @@ describe( 'UpcastDispatcher', () => { spy(); } ); - dispatcher.convert( new ViewDocumentFragment() ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer ) ); sinon.assert.calledOnce( spy ); } ); @@ -622,7 +624,7 @@ describe( 'UpcastDispatcher', () => { spy(); } ); - dispatcher.convert( new ViewDocumentFragment(), [ '$root', 'paragraph' ] ); + model.change( writer => dispatcher.convert( new ViewDocumentFragment(), writer, [ '$root', 'paragraph' ] ) ); sinon.assert.calledOnce( spy ); } ); } ); From 9d11c6473c0755719f6bd12f45e672fa19c57b7f Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Wed, 14 Feb 2018 17:35:50 +0100 Subject: [PATCH 79/89] Docs: View class overview. [skip ci] --- src/view/view.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/view/view.js b/src/view/view.js index 77eeeb79c..e9f451f55 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -27,17 +27,19 @@ import { injectQuirksHandling } from './filler'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; /** - * 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}. + * Editor's view controller class. Its main responsibility is DOM - View management for editing purposes, to provide + * abstraction over the DOM structure and events and hide all browsers quirks. * - * 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. + * View controller renders view document to DOM whenever view structure changes. To determine when view can be rendered, + * all changes need to be done using the {@link module:engine/view/view~View#change} method, using + * {@link module:engine/view/writer~Writer}: * - * 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}. + * view.change( writer => { + * writer.insert( position, writer.createText( 'foo' ) ); + * } ); * + * View controller also register {@link module:engine/view/observer/observer~Observer observers} which observes changes + * on DOM and fire events on the {@link module:engine/view/document~Document Document}. * Note that the following observers are added by the class constructor and are always available: * * * {@link module:engine/view/observer/mutationobserver~MutationObserver}, @@ -46,6 +48,11 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; * * {@link module:engine/view/observer/keyobserver~KeyObserver}, * * {@link module:engine/view/observer/fakeselectionobserver~FakeSelectionObserver}. * + * This class also {@link module:engine/view/view~View#attachDomRoot bind DOM and View elements}. + * + * If you do not need full DOM - View management, and want to only transform the tree of view elements to the DOM + * elements you do not need this controller, you can use the {@link module:engine/view/domconverter~DomConverter DomConverter}. + * * @mixes module:utils/observablemixin~ObservableMixin */ export default class View { @@ -285,10 +292,10 @@ export default class View { * after all changes are applied. * * view.change( writer => { - * writer.insert( position1, writer.createText( 'foo' ); + * writer.insert( position1, writer.createText( 'foo' ) ); * * view.change( writer => { - * writer.insert( position2, writer.createText( 'bar' ); + * writer.insert( position2, writer.createText( 'bar' ) ); * } ); * * writer.remove( range ); From 27c79069cbdc9978960eb40fcfdd7e2a340ade86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 17:52:16 +0100 Subject: [PATCH 80/89] Get rid of model parameter from downcast dispatcher. --- src/controller/datacontroller.js | 2 +- src/controller/editingcontroller.js | 6 +++-- src/conversion/downcastdispatcher.js | 21 ++++++---------- src/dev-utils/model.js | 4 ++-- .../downcast-selection-converters.js | 24 +++++++++---------- tests/conversion/downcastdispatcher.js | 22 ++++++++--------- 6 files changed, 37 insertions(+), 42 deletions(-) diff --git a/src/controller/datacontroller.js b/src/controller/datacontroller.js index 4c8942f30..66fa77d37 100644 --- a/src/controller/datacontroller.js +++ b/src/controller/datacontroller.js @@ -76,7 +76,7 @@ export default class DataController { * @readonly * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} */ - this.downcastDispatcher = new DowncastDispatcher( this.model, { + this.downcastDispatcher = new DowncastDispatcher( { mapper: this.mapper } ); this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } ); diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index 21f0b123d..44dd22c50 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -70,16 +70,18 @@ export default class EditingController { * @readonly * @member {module:engine/conversion/downcastdispatcher~DowncastDispatcher} #downcastDispatcher */ - this.downcastDispatcher = new DowncastDispatcher( this.model, { + this.downcastDispatcher = new DowncastDispatcher( { mapper: this.mapper } ); const doc = this.model.document; + const selection = doc.selection; + const markers = this.model.markers; this.listenTo( doc, 'change', () => { this.view.change( writer => { this.downcastDispatcher.convertChanges( doc.differ, writer ); - this.downcastDispatcher.convertSelection( doc.selection, writer ); + this.downcastDispatcher.convertSelection( selection, markers, writer ); } ); }, { priority: 'low' } ); diff --git a/src/conversion/downcastdispatcher.js b/src/conversion/downcastdispatcher.js index 1f19325a1..0297ad0f9 100644 --- a/src/conversion/downcastdispatcher.js +++ b/src/conversion/downcastdispatcher.js @@ -105,18 +105,9 @@ export default class DowncastDispatcher { /** * Creates a `DowncastDispatcher` instance. * - * @param {module:engine/model/model~Model} model Data model. * @param {Object} [conversionApi] Interface passed by dispatcher to the events calls. */ - constructor( model, conversionApi = {} ) { - /** - * Data model instance bound with this dispatcher. - * - * @private - * @member {module:engine/model/model~Model} - */ - this._model = model; - + constructor( conversionApi = {} ) { /** * Interface passed by dispatcher to the events callbacks. * @@ -250,12 +241,14 @@ export default class DowncastDispatcher { * @fires addMarker * @fires attribute * @param {module:engine/model/selection~Selection} selection Selection to convert. + * @param {module:engine/model/selection~Selection} Array markers + * Array of markers containing model markers. * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document. */ - convertSelection( selection, writer ) { + convertSelection( selection, markers, writer ) { this.conversionApi.writer = writer; - const markers = Array.from( this._model.markers.getMarkersAtPosition( selection.getFirstPosition() ) ); - const consumable = this._createSelectionConsumable( selection, markers ); + const markersAtSelection = Array.from( markers.getMarkersAtPosition( selection.getFirstPosition() ) ); + const consumable = this._createSelectionConsumable( selection, markersAtSelection ); this.fire( 'selection', { selection }, consumable, this.conversionApi ); @@ -263,7 +256,7 @@ export default class DowncastDispatcher { return; } - for ( const marker of markers ) { + for ( const marker of markersAtSelection ) { const markerRange = marker.getRange(); if ( !shouldMarkerChangeBeConverted( selection.getFirstPosition(), marker, this.conversionApi.mapper ) ) { diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 4321128ab..a66b7c87c 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -202,7 +202,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) { viewDocument.roots.add( viewRoot ); // Create and setup downcast dispatcher. - const downcastDispatcher = new DowncastDispatcher( model, { mapper } ); + const downcastDispatcher = new DowncastDispatcher( { mapper } ); // Bind root elements. mapper.bindElements( node.root, viewRoot ); @@ -228,7 +228,7 @@ export function stringify( node, selectionOrPositionOrRange = null ) { // Convert model selection to view selection. if ( selection ) { - downcastDispatcher.convertSelection( selection, writer ); + downcastDispatcher.convertSelection( selection, model.markers, writer ); } // Parse view to data string. diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index 48c2c81c2..20def2bd7 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -54,7 +54,7 @@ describe( 'downcast-selection-converters', () => { highlightDescriptor = { class: 'marker', priority: 1 }; - dispatcher = new DowncastDispatcher( model, { mapper, viewSelection } ); + dispatcher = new DowncastDispatcher( { mapper, viewSelection } ); dispatcher.on( 'insert:$text', insertText() ); @@ -209,7 +209,7 @@ describe( 'downcast-selection-converters', () => { view.change( writer => { dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. @@ -234,7 +234,7 @@ describe( 'downcast-selection-converters', () => { view.change( writer => { dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. @@ -261,7 +261,7 @@ describe( 'downcast-selection-converters', () => { view.change( writer => { dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. @@ -286,7 +286,7 @@ describe( 'downcast-selection-converters', () => { view.change( writer => { dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. @@ -311,7 +311,7 @@ describe( 'downcast-selection-converters', () => { // Convert model to view. view.change( writer => { - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. @@ -336,7 +336,7 @@ describe( 'downcast-selection-converters', () => { const uiElement = new ViewUIElement( 'span' ); viewRoot.insertChildren( 1, uiElement ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. @@ -360,7 +360,7 @@ describe( 'downcast-selection-converters', () => { // Add ui element to view. const uiElement = new ViewUIElement( 'span' ); viewRoot.insertChildren( 1, uiElement, writer ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. @@ -441,7 +441,7 @@ describe( 'downcast-selection-converters', () => { writer.setSelection( modelRange ); } ); - dispatcher.convertSelection( modelDoc.selection, writer ); + dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); } ); expect( viewSelection.rangeCount ).to.equal( 1 ); @@ -467,7 +467,7 @@ describe( 'downcast-selection-converters', () => { writer.setSelection( modelRange ); } ); - dispatcher.convertSelection( modelDoc.selection, writer ); + dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); } ); expect( viewSelection.rangeCount ).to.equal( 1 ); @@ -483,7 +483,7 @@ describe( 'downcast-selection-converters', () => { view.change( writer => { writer.setFakeSelection( true ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); expect( viewSelection.isFake ).to.be.false; } ); @@ -594,7 +594,7 @@ describe( 'downcast-selection-converters', () => { // Convert model to view. view.change( writer => { dispatcher.convertInsert( ModelRange.createIn( modelRoot ), writer ); - dispatcher.convertSelection( docSelection, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); } ); // Stringify view and check if it is same as expected. diff --git a/tests/conversion/downcastdispatcher.js b/tests/conversion/downcastdispatcher.js index bd2045ee2..7e4866054 100644 --- a/tests/conversion/downcastdispatcher.js +++ b/tests/conversion/downcastdispatcher.js @@ -20,7 +20,7 @@ describe( 'DowncastDispatcher', () => { model = new Model(); view = new View(); doc = model.document; - dispatcher = new DowncastDispatcher( model ); + dispatcher = new DowncastDispatcher(); root = doc.createRoot(); differStub = { @@ -33,7 +33,7 @@ describe( 'DowncastDispatcher', () => { describe( 'constructor()', () => { it( 'should create DowncastDispatcher with given api', () => { const apiObj = {}; - const dispatcher = new DowncastDispatcher( model, { apiObj } ); + const dispatcher = new DowncastDispatcher( { apiObj } ); expect( dispatcher.conversionApi.apiObj ).to.equal( apiObj ); } ); @@ -264,7 +264,7 @@ describe( 'DowncastDispatcher', () => { it( 'should fire selection event', () => { sinon.spy( dispatcher, 'fire' ); - dispatcher.convertSelection( doc.selection, [] ); + dispatcher.convertSelection( doc.selection, model.markers, [] ); expect( dispatcher.fire.calledWith( 'selection', @@ -284,7 +284,7 @@ describe( 'DowncastDispatcher', () => { expect( consumable.test( data.selection, 'attribute:italic' ) ).to.be.null; } ); - dispatcher.convertSelection( doc.selection, [] ); + dispatcher.convertSelection( doc.selection, model.markers, [] ); } ); it( 'should not fire attributes events for non-collapsed selection', () => { @@ -295,7 +295,7 @@ describe( 'DowncastDispatcher', () => { sinon.spy( dispatcher, 'fire' ); - dispatcher.convertSelection( doc.selection, [] ); + dispatcher.convertSelection( doc.selection, model.markers, [] ); expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false; expect( dispatcher.fire.calledWith( 'attribute:italic' ) ).to.be.false; @@ -314,7 +314,7 @@ describe( 'DowncastDispatcher', () => { sinon.spy( dispatcher, 'fire' ); - dispatcher.convertSelection( doc.selection, [] ); + dispatcher.convertSelection( doc.selection, model.markers, [] ); expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.true; } ); @@ -337,7 +337,7 @@ describe( 'DowncastDispatcher', () => { sinon.spy( dispatcher, 'fire' ); - dispatcher.convertSelection( doc.selection, [] ); + dispatcher.convertSelection( doc.selection, model.markers, [] ); expect( dispatcher.fire.calledWith( 'attribute:bold' ) ).to.be.false; } ); @@ -353,7 +353,7 @@ describe( 'DowncastDispatcher', () => { sinon.spy( dispatcher, 'fire' ); const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) ); - dispatcher.convertSelection( doc.selection, markers ); + dispatcher.convertSelection( doc.selection, model.markers, markers ); expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.true; } ); @@ -366,7 +366,7 @@ describe( 'DowncastDispatcher', () => { sinon.spy( dispatcher, 'fire' ); const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) ); - dispatcher.convertSelection( doc.selection, markers ); + dispatcher.convertSelection( doc.selection, model.markers, markers ); expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false; } ); @@ -407,7 +407,7 @@ describe( 'DowncastDispatcher', () => { const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) ); - dispatcher.convertSelection( doc.selection, markers ); + dispatcher.convertSelection( doc.selection, model.markers, markers ); expect( dispatcher.fire.calledWith( 'addMarker:name' ) ).to.be.false; } ); @@ -428,7 +428,7 @@ describe( 'DowncastDispatcher', () => { } ); const markers = Array.from( model.markers.getMarkersAtPosition( doc.selection.getFirstPosition() ) ); - dispatcher.convertSelection( doc.selection, markers ); + dispatcher.convertSelection( doc.selection, model.markers, markers ); expect( dispatcher.fire.calledWith( 'addMarker:foo' ) ).to.be.true; expect( dispatcher.fire.calledWith( 'addMarker:bar' ) ).to.be.false; From f63f2a1d0d411a0a10fb341087c5b270881466fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Wed, 14 Feb 2018 18:43:42 +0100 Subject: [PATCH 81/89] Passing writer to highlight methods. --- src/conversion/downcast-converters.js | 4 ++-- tests/conversion/downcast-converters.js | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/conversion/downcast-converters.js b/src/conversion/downcast-converters.js index 345350d44..40e7b5d52 100644 --- a/src/conversion/downcast-converters.js +++ b/src/conversion/downcast-converters.js @@ -899,7 +899,7 @@ export function highlightElement( highlightDescriptor ) { consumable.consume( value.item, evt.name ); } - viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor ); + viewElement.getCustomProperty( 'addHighlight' )( viewElement, descriptor, conversionApi.writer ); } }; } @@ -947,7 +947,7 @@ export function removeHighlight( highlightDescriptor ) { // First, iterate through all items and remove highlight from those container elements that have custom highlight handling. for ( const item of items ) { if ( item.is( 'containerElement' ) && item.getCustomProperty( 'removeHighlight' ) ) { - item.getCustomProperty( 'removeHighlight' )( item, descriptor.id ); + item.getCustomProperty( 'removeHighlight' )( item, descriptor.id, conversionApi.writer ); // If container element had custom handling, remove all it's children from further processing. for ( const descendant of ViewRange.createIn( item ) ) { diff --git a/tests/conversion/downcast-converters.js b/tests/conversion/downcast-converters.js index de29d8839..13d6e9db2 100644 --- a/tests/conversion/downcast-converters.js +++ b/tests/conversion/downcast-converters.js @@ -1302,16 +1302,12 @@ describe( 'downcast-converters', () => { dispatcher.on( 'insert:div', insertElement( () => { const viewContainer = new ViewContainerElement( 'div' ); - viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor ) => { - controller.view.change( writer => { - writer.addClass( descriptor.class, element ); - } ); + viewContainer._setCustomProperty( 'addHighlight', ( element, descriptor, writer ) => { + writer.addClass( descriptor.class, element ); } ); - viewContainer._setCustomProperty( 'removeHighlight', element => { - controller.view.change( writer => { - writer.setAttribute( 'class', '', element ); - } ); + viewContainer._setCustomProperty( 'removeHighlight', ( element, id, writer ) => { + writer.setAttribute( 'class', '', element ); } ); return viewContainer; From b1580a080cfea73023ae87c9e2471c10bebf4707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Thu, 15 Feb 2018 13:08:56 +0100 Subject: [PATCH 82/89] Updated highlighting manual test. --- tests/manual/highlight.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 81f819e2d..2b26018eb 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -39,7 +39,7 @@ class FancyWidget extends Plugin { init() { const editor = this.editor; const schema = editor.model.schema; - const data = editor.data; + const conversion = editor.conversion; // Configure schema. schema.register( 'fancywidget', { @@ -47,21 +47,22 @@ class FancyWidget extends Plugin { } ); schema.extend( 'fancywidget', { allowIn: '$root' } ); - downcastElementToElement( { + conversion.for( 'editingDowncast' ).add( downcastElementToElement( { model: 'fancywidget', view: ( modelItem, consumable, conversionApi ) => { const viewWriter = conversionApi.writer; const widgetElement = viewWriter.createContainerElement( 'figure', { class: 'fancy-widget' } ); viewWriter.insert( ViewPosition.createAt( widgetElement ), viewWriter.createText( 'widget' ) ); - return toWidget( widgetElement ); + return toWidget( widgetElement, viewWriter ); } - } )( data.downcastDispatcher ); + } ) ); - upcastElementToElement( { - view: 'figure', - model: 'fancywidget' - } )( data.upcastDispatcher ); + conversion.for( 'upcast' ) + .add( upcastElementToElement( { + view: 'figure', + model: 'fancywidget' + } ) ); } } @@ -72,12 +73,12 @@ ClassicEditor.create( global.document.querySelector( '#editor' ), { .then( editor => { window.editor = editor; - downcastMarkerToHighlight( { + editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( { model: 'marker', view: data => ( { class: 'highlight-' + data.markerName.split( ':' )[ 1 ] } ) - } ); + } ) ); document.getElementById( 'add-marker-yellow' ).addEventListener( 'mousedown', evt => { addMarker( editor, 'yellow' ); From 4b33b87bfe6bf143e52724b38035fd533635cc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Thu, 15 Feb 2018 13:17:22 +0100 Subject: [PATCH 83/89] Fixed nested editable manual test. --- tests/manual/markers.js | 4 ++-- tests/manual/nestededitable.js | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/manual/markers.js b/tests/manual/markers.js index fcf7da1b3..bc7f4594a 100644 --- a/tests/manual/markers.js +++ b/tests/manual/markers.js @@ -35,7 +35,7 @@ ClassicEditor window.editor = editor; model = editor.model; - downcastMarkerToHighlight( { + editor.conversion.for( 'editingDowncast' ).add( downcastMarkerToHighlight( { model: 'highlight', view: data => { const color = data.markerName.split( ':' )[ 1 ]; @@ -45,7 +45,7 @@ ClassicEditor priority: 1 }; } - } ); + } ) ); window.document.getElementById( 'add-yellow' ).addEventListener( 'mousedown', e => { e.preventDefault(); diff --git a/tests/manual/nestededitable.js b/tests/manual/nestededitable.js index 05c4cc057..08ba48b1e 100644 --- a/tests/manual/nestededitable.js +++ b/tests/manual/nestededitable.js @@ -13,7 +13,6 @@ import { downcastElementToElement } from '../../src/conversion/downcast-converters'; -import ViewEditableElement from '../../src/view/editableelement'; import { getData } from '../../src/dev-utils/model'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -30,7 +29,6 @@ class NestedEditable extends Plugin { init() { const editor = this.editor; const editing = editor.editing; - const viewDocument = editing.view; const schema = editor.model.schema; schema.register( 'figure', { @@ -60,15 +58,15 @@ class NestedEditable extends Plugin { editor.conversion.for( 'downcast' ).add( downcastElementToElement( { model: 'figcaption', - view: () => { - const element = new ViewEditableElement( 'figcaption', { contenteditable: 'true' } ); - element.document = viewDocument; + view: ( modelItem, consumable, conversionApi ) => { + const viewWriter = conversionApi.writer; + const element = viewWriter.createEditableElement( 'figcaption', { contenteditable: 'true' } ); element.on( 'change:isFocused', ( evt, property, is ) => { if ( is ) { - element.addClass( 'focused' ); + editing.view.change( writer => writer.addClass( 'focused', element ) ); } else { - element.removeClass( 'focused' ); + editing.view.change( writer => writer.removeClass( 'focused', element ) ); } } ); From 5ac0d62ee8aceea0d01e56f1cb58b970e0c881bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Thu, 15 Feb 2018 13:39:53 +0100 Subject: [PATCH 84/89] Updated manual test for ckeditor5-721. --- tests/manual/tickets/ckeditor5-721/1.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/manual/tickets/ckeditor5-721/1.js b/tests/manual/tickets/ckeditor5-721/1.js index 1a5c1c7e7..5c4d3b51c 100644 --- a/tests/manual/tickets/ckeditor5-721/1.js +++ b/tests/manual/tickets/ckeditor5-721/1.js @@ -12,11 +12,9 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; -import AttributeContainer from '../../../../src/view/attributeelement'; -import ViewContainer from '../../../../src/view/containerelement'; +import ViewPosition from '../../../../src/view/position'; import { downcastElementToElement } from '../../../../src/conversion/downcast-converters'; import { setData } from '../../../../src/dev-utils/model'; -import ViewEditable from '../../../../src/view/editableelement'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -45,16 +43,19 @@ ClassicEditor editor.conversion.for( 'downcast' ) .add( downcastElementToElement( { model: 'widget', - view: () => { - const b = new AttributeContainer( 'b' ); - const div = new ViewContainer( 'div', null, b ); + view: ( modelItem, consumable, conversionApi ) => { + const writer = conversionApi.writer; + const b = writer.createAttributeElement( 'b' ); + const div = writer.createContainerElement( 'div' ); - return toWidget( div, { label: 'element label' } ); + writer.insert( ViewPosition.createAt( div ), b ); + + return toWidget( div, writer, { label: 'element label' } ); } } ) ) .add( downcastElementToElement( { model: 'nested', - view: () => new ViewEditable( 'figcaption', { contenteditable: true } ) + view: ( item, consumable, api ) => api.writer.createEditableElement( 'figcaption', { contenteditable: true } ) } ) ); setData( editor.model, From b1190885596410a790b4a23a825174dca596ad47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Thu, 15 Feb 2018 15:07:59 +0100 Subject: [PATCH 85/89] Updated fake selection manual test. --- tests/view/manual/fakeselection.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/view/manual/fakeselection.js b/tests/view/manual/fakeselection.js index 68738afc2..315edddff 100644 --- a/tests/view/manual/fakeselection.js +++ b/tests/view/manual/fakeselection.js @@ -58,24 +58,20 @@ viewDocument.selection.on( 'change', () => { const lastPos = viewDocument.selection.getLastPosition(); if ( firstPos && lastPos && firstPos.nodeAfter == viewStrong && lastPos.nodeBefore == viewStrong ) { - viewStrong.addClass( 'selected' ); + view.change( writer => writer.addClass( 'selected', viewStrong ) ); } else { - viewStrong.removeClass( 'selected' ); + view.change( writer => writer.removeClass( 'selected', viewStrong ) ); } } ); viewDocument.on( 'focus', () => { - view.change( () => { - viewStrong.addClass( 'focused' ); - } ); + view.change( writer => writer.addClass( 'focused', viewStrong ) ); console.log( 'The document was focused.' ); } ); viewDocument.on( 'blur', () => { - view.change( () => { - viewStrong.removeClass( 'focused' ); - } ); + view.change( writer => writer.removeClass( 'focused', viewStrong ) ); console.log( 'The document was blurred.' ); } ); From 61edf032a8932f691d6c90b919a585fcd74a605a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Thu, 15 Feb 2018 15:22:13 +0100 Subject: [PATCH 86/89] Updated UIElement manual test. --- tests/view/manual/uielement.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/view/manual/uielement.js b/tests/view/manual/uielement.js index ddb8b1981..3d99db59e 100644 --- a/tests/view/manual/uielement.js +++ b/tests/view/manual/uielement.js @@ -16,29 +16,25 @@ import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import Position from '../../../src/view/position'; function createEndingUIElement( writer ) { - const element = writer.createUIElement( 'span' ); - - element.render = function( domDocument ) { + const element = writer.createUIElement( 'span', null, function( domDocument ) { const root = this.toDomElement( domDocument ); root.classList.add( 'ui-element' ); root.innerHTML = 'END OF PARAGRAPH'; return root; - }; + } ); return element; } function createMiddleUIElement( writer ) { - const element = writer.createUIElement( 'span' ); - - element.render = function( domDocument ) { + const element = writer.createUIElement( 'span', null, function( domDocument ) { const root = this.toDomElement( domDocument ); root.classList.add( 'ui-element' ); root.innerHTML = 'X'; return root; - }; + } ); return element; } From 47371daf20bde313748ea82d60cdd7a9a0d65ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kup=C5=9B?= Date: Thu, 15 Feb 2018 16:50:32 +0100 Subject: [PATCH 87/89] Fixed docs. --- src/view/view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/view.js b/src/view/view.js index e9f451f55..8a493152e 100644 --- a/src/view/view.js +++ b/src/view/view.js @@ -304,7 +304,7 @@ export default class View { * Change block is executed immediately. * * When the outermost change block is done and rendering to DOM is over it fires - * {@link module:engine/view/document~Document#event:change} event. + * {@link module:engine/view/view~View#event:render} event. * * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `applying-view-changes-on-rendering` when * change block is used after rendering to DOM has started. From 08eb9c969057bbc5a8a39f4ac26ac8a5b440dbb9 Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Fri, 16 Feb 2018 10:45:48 +0100 Subject: [PATCH 88/89] Removed unneeded code. --- src/conversion/downcastdispatcher.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/conversion/downcastdispatcher.js b/src/conversion/downcastdispatcher.js index 0297ad0f9..00c4068a7 100644 --- a/src/conversion/downcastdispatcher.js +++ b/src/conversion/downcastdispatcher.js @@ -123,8 +123,6 @@ export default class DowncastDispatcher { * @param {module:engine/view/writer~Writer} writer View writer that should be used to modify view document. */ convertChanges( differ, writer ) { - this.conversionApi.writer = writer; - // Convert changes that happened on model tree. for ( const entry of differ.getChanges() ) { if ( entry.type == 'insert' ) { From 8a07e761de42d53d6c48b5c428eda3a18f818e4d Mon Sep 17 00:00:00 2001 From: Piotr Jasiun Date: Fri, 16 Feb 2018 10:54:50 +0100 Subject: [PATCH 89/89] Docs: use proper writer methods. --- src/view/element.js | 2 +- src/view/writer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view/element.js b/src/view/element.js index 210530b74..4361a180b 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -539,7 +539,7 @@ export default class Element extends Node { * * For example: * - * const element = new ViewElement( 'foo', { + * const element = writer.createContainerElement( 'foo', { * banana: '10', * apple: '20', * style: 'color: red; border-color: white;', diff --git a/src/view/writer.js b/src/view/writer.js index 32e48fc6b..1b54e87c3 100644 --- a/src/view/writer.js +++ b/src/view/writer.js @@ -53,7 +53,7 @@ export default class Writer { * writer.setSelection( position ); * * // Sets collapsed range on the given item. - * const paragraph = writer.createElement( 'paragraph' ); + * const paragraph = writer.createContainerElement( 'paragraph' ); * writer.setSelection( paragraph, offset ); * * // Removes all ranges.