From c4cb398befc302466d4c5fce4bc1d896c77b4571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:13:37 +0100 Subject: [PATCH 1/8] Move downcast conversion helpers for selection to downcasthelpers.js. --- src/controller/editingcontroller.js | 3 +- .../downcast-selection-converters.js | 117 ------------------ src/conversion/downcasthelpers.js | 117 ++++++++++++++++++ src/dev-utils/model.js | 9 +- .../downcast-selection-converters.js | 12 +- 5 files changed, 130 insertions(+), 128 deletions(-) diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index b504ff4b5..d7a7ed732 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -11,9 +11,8 @@ import RootEditableElement from '../view/rooteditableelement'; import View from '../view/view'; import Mapper from '../conversion/mapper'; import DowncastDispatcher from '../conversion/downcastdispatcher'; -import { insertText, remove } from '../conversion/downcasthelpers'; +import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers'; import { convertSelectionChange } from '../conversion/upcast-selection-converters'; -import { clearAttributes, convertCollapsedSelection, convertRangeSelection } from '../conversion/downcast-selection-converters'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; diff --git a/src/conversion/downcast-selection-converters.js b/src/conversion/downcast-selection-converters.js index 9024ec87d..59a907dcf 100644 --- a/src/conversion/downcast-selection-converters.js +++ b/src/conversion/downcast-selection-converters.js @@ -10,120 +10,3 @@ * * @module engine/conversion/downcast-selection-converters */ - -/** - * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection} - * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate - * value from the `consumable` object and maps model positions from the selection to view positions. - * - * modelDispatcher.on( 'selection', convertRangeSelection() ); - * - * @returns {Function} Selection converter. - */ -export function convertRangeSelection() { - return ( evt, data, conversionApi ) => { - const selection = data.selection; - - if ( selection.isCollapsed ) { - return; - } - - if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { - return; - } - - const viewRanges = []; - - for ( const range of selection.getRanges() ) { - const viewRange = conversionApi.mapper.toViewRange( range ); - viewRanges.push( viewRange ); - } - - conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } ); - }; -} - -/** - * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to - * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate - * value from the `consumable` object, maps the model selection position to the view position and breaks - * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position. - * - * modelDispatcher.on( 'selection', convertCollapsedSelection() ); - * - * An example of the view state before and after converting the collapsed selection: - * - *

f^oobar

- * ->

f^oobar

- * - * By breaking attribute elements like ``, the selection is in a correct element. Then, when the selection attribute is - * converted, broken attributes might be merged again, or the position where the selection is may be wrapped - * with different, appropriate attribute elements. - * - * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up - * by merging attributes. - * - * @returns {Function} Selection converter. - */ -export function convertCollapsedSelection() { - return ( evt, data, conversionApi ) => { - const selection = data.selection; - - if ( !selection.isCollapsed ) { - return; - } - - if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { - return; - } - - const viewWriter = conversionApi.writer; - const modelPosition = selection.getFirstPosition(); - const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); - const brokenPosition = viewWriter.breakAttributes( viewPosition ); - - viewWriter.setSelection( brokenPosition ); - }; -} - -/** - * Function factory that creates a converter which clears artifacts after the previous - * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty - * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end - * positions of all ranges. - * - *

^

- * ->

^

- * - *

foo^barbar

- * ->

foo^barbar

- * - *

foo^barbar

- * ->

foo^barbar

- * - * This listener should be assigned before any converter for the new selection: - * - * modelDispatcher.on( 'selection', clearAttributes() ); - * - * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection} - * which does the opposite by breaking attributes in the selection position. - * - * @returns {Function} Selection converter. - */ -export function clearAttributes() { - return ( evt, data, conversionApi ) => { - const viewWriter = conversionApi.writer; - const viewSelection = viewWriter.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. - if ( range.end.parent.document ) { - conversionApi.writer.mergeAttributes( range.start ); - } - } - } - viewWriter.setSelection( null ); - }; -} diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index e8cc47832..734486938 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -421,6 +421,123 @@ export function createViewElementFromHighlightDescriptor( descriptor ) { return viewElement; } +/** + * Function factory that creates a converter which converts a non-collapsed {@link module:engine/model/selection~Selection model selection} + * to a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate + * value from the `consumable` object and maps model positions from the selection to view positions. + * + * modelDispatcher.on( 'selection', convertRangeSelection() ); + * + * @returns {Function} Selection converter. + */ +export function convertRangeSelection() { + return ( evt, data, conversionApi ) => { + const selection = data.selection; + + if ( selection.isCollapsed ) { + return; + } + + if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { + return; + } + + const viewRanges = []; + + for ( const range of selection.getRanges() ) { + const viewRange = conversionApi.mapper.toViewRange( range ); + viewRanges.push( viewRange ); + } + + conversionApi.writer.setSelection( viewRanges, { backward: selection.isBackward } ); + }; +} + +/** + * Function factory that creates a converter which converts a collapsed {@link module:engine/model/selection~Selection model selection} to + * a {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate + * value from the `consumable` object, maps the model selection position to the view position and breaks + * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position. + * + * modelDispatcher.on( 'selection', convertCollapsedSelection() ); + * + * An example of the view state before and after converting the collapsed selection: + * + *

f^oobar

+ * ->

f^oobar

+ * + * By breaking attribute elements like ``, the selection is in a correct element. Then, when the selection attribute is + * converted, broken attributes might be merged again, or the position where the selection is may be wrapped + * with different, appropriate attribute elements. + * + * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up + * by merging attributes. + * + * @returns {Function} Selection converter. + */ +export function convertCollapsedSelection() { + return ( evt, data, conversionApi ) => { + const selection = data.selection; + + if ( !selection.isCollapsed ) { + return; + } + + if ( !conversionApi.consumable.consume( selection, 'selection' ) ) { + return; + } + + const viewWriter = conversionApi.writer; + const modelPosition = selection.getFirstPosition(); + const viewPosition = conversionApi.mapper.toViewPosition( modelPosition ); + const brokenPosition = viewWriter.breakAttributes( viewPosition ); + + viewWriter.setSelection( brokenPosition ); + }; +} + +/** + * Function factory that creates a converter which clears artifacts after the previous + * {@link module:engine/model/selection~Selection model selection} conversion. It removes all empty + * {@link module:engine/view/attributeelement~AttributeElement view attribute elements} and merges sibling attributes at all start and end + * positions of all ranges. + * + *

^

+ * ->

^

+ * + *

foo^barbar

+ * ->

foo^barbar

+ * + *

foo^barbar

+ * ->

foo^barbar

+ * + * This listener should be assigned before any converter for the new selection: + * + * modelDispatcher.on( 'selection', clearAttributes() ); + * + * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection} + * which does the opposite by breaking attributes in the selection position. + * + * @returns {Function} Selection converter. + */ +export function clearAttributes() { + return ( evt, data, conversionApi ) => { + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.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. + if ( range.end.parent.document ) { + conversionApi.writer.mergeAttributes( range.start ); + } + } + } + viewWriter.setSelection( null ); + }; +} + /** * Function factory that creates a converter which converts set/change/remove attribute changes from the model to the view. * It can also be used to convert selection attributes. In that case, an empty attribute element will be created and the diff --git a/src/dev-utils/model.js b/src/dev-utils/model.js index 4427c4097..7ec10940e 100644 --- a/src/dev-utils/model.js +++ b/src/dev-utils/model.js @@ -29,10 +29,13 @@ import DowncastDispatcher from '../conversion/downcastdispatcher'; import UpcastDispatcher from '../conversion/upcastdispatcher'; import Mapper from '../conversion/mapper'; import { - convertRangeSelection, convertCollapsedSelection, -} from '../conversion/downcast-selection-converters'; -import { insertElement, insertText, insertUIElement, wrap } from '../conversion/downcasthelpers'; + convertRangeSelection, + insertElement, + insertText, + insertUIElement, + wrap +} from '../conversion/downcasthelpers'; import { isPlainObject } from 'lodash-es'; import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js index fffc3e5d5..92b956137 100644 --- a/tests/conversion/downcast-selection-converters.js +++ b/tests/conversion/downcast-selection-converters.js @@ -10,13 +10,13 @@ import ViewUIElement from '../../src/view/uielement'; import Mapper from '../../src/conversion/mapper'; import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; -import { - convertRangeSelection, - convertCollapsedSelection, - clearAttributes, -} from '../../src/conversion/downcast-selection-converters'; -import DowncastHelpers, { insertText } from '../../src/conversion/downcasthelpers'; +import DowncastHelpers, { + clearAttributes, + convertCollapsedSelection, + convertRangeSelection, + insertText +} from '../../src/conversion/downcasthelpers'; import createViewRoot from '../view/_utils/createroot'; import { stringify as stringifyView } from '../../src/dev-utils/view'; From 0520e31336e1aa04b14a3c3b665e9c3b171e7dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:50:35 +0100 Subject: [PATCH 2/8] Remove downcast-selection-converters.js. --- .../downcast-selection-converters.js | 12 - .../downcast-selection-converters.js | 594 ------------------ tests/conversion/downcasthelpers.js | 589 ++++++++++++++++- 3 files changed, 585 insertions(+), 610 deletions(-) delete mode 100644 src/conversion/downcast-selection-converters.js delete mode 100644 tests/conversion/downcast-selection-converters.js diff --git a/src/conversion/downcast-selection-converters.js b/src/conversion/downcast-selection-converters.js deleted file mode 100644 index 59a907dcf..000000000 --- a/src/conversion/downcast-selection-converters.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * Contains {@link module:engine/model/selection~Selection model selection} to - * {@link module:engine/view/documentselection~DocumentSelection view selection} converters for - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher}. - * - * @module engine/conversion/downcast-selection-converters - */ diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js deleted file mode 100644 index 92b956137..000000000 --- a/tests/conversion/downcast-selection-converters.js +++ /dev/null @@ -1,594 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import Model from '../../src/model/model'; - -import View from '../../src/view/view'; -import ViewUIElement from '../../src/view/uielement'; - -import Mapper from '../../src/conversion/mapper'; -import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; - -import DowncastHelpers, { - clearAttributes, - convertCollapsedSelection, - convertRangeSelection, - insertText -} from '../../src/conversion/downcasthelpers'; - -import createViewRoot from '../view/_utils/createroot'; -import { stringify as stringifyView } from '../../src/dev-utils/view'; -import { setData as setModelData } from '../../src/dev-utils/model'; - -describe( 'downcast-selection-converters', () => { - let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers; - - beforeEach( () => { - model = new Model(); - modelDoc = model.document; - modelRoot = modelDoc.createRoot(); - docSelection = modelDoc.selection; - - model.schema.extend( '$text', { allowIn: '$root' } ); - - view = new View(); - viewDoc = view.document; - viewRoot = createViewRoot( viewDoc ); - viewSelection = viewDoc.selection; - - mapper = new Mapper(); - mapper.bindElements( modelRoot, viewRoot ); - - dispatcher = new DowncastDispatcher( { mapper, viewSelection } ); - - dispatcher.on( 'insert:$text', insertText() ); - - downcastHelpers = new DowncastHelpers( dispatcher ); - downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); - downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } ); - - // Default selection converters. - dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } ); - dispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } ); - dispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } ); - } ); - - afterEach( () => { - view.destroy(); - } ); - - describe( 'default converters', () => { - describe( 'range selection', () => { - it( 'in same container', () => { - test( - [ 1, 4 ], - 'foobar', - 'f{oob}ar' - ); - } ); - - it( 'in same container with unicode characters', () => { - test( - [ 2, 6 ], - 'நிலைக்கு', - 'நி{லைக்}கு' - ); - } ); - - it( 'in same container, over attribute', () => { - test( - [ 1, 5 ], - 'fo<$text bold="true">obar', - 'f{ooba}r' - ); - } ); - - it( 'in same container, next to attribute', () => { - test( - [ 1, 2 ], - 'fo<$text bold="true">obar', - 'f{o}obar' - ); - } ); - - it( 'in same attribute', () => { - test( - [ 2, 4 ], - 'f<$text bold="true">oobar', - 'fo{ob}ar' - ); - } ); - - it( 'in same attribute, selection same as attribute', () => { - test( - [ 2, 4 ], - 'fo<$text bold="true">obar', - 'fo{ob}ar' - ); - } ); - - it( 'starts in text node, ends in attribute #1', () => { - test( - [ 1, 3 ], - 'fo<$text bold="true">obar', - 'f{oo}bar' - ); - } ); - - it( 'starts in text node, ends in attribute #2', () => { - test( - [ 1, 4 ], - 'fo<$text bold="true">obar', - 'f{oob}ar' - ); - } ); - - it( 'starts in attribute, ends in text node', () => { - test( - [ 3, 5 ], - 'fo<$text bold="true">obar', - 'foo{ba}r' - ); - } ); - - it( 'consumes consumable values properly', () => { - // Add callback that will fire before default ones. - // This should prevent default callback doing anything. - dispatcher.on( 'selection', ( evt, data, conversionApi ) => { - expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; - }, { priority: 'high' } ); - - // Similar test case as the first in this suite. - test( - [ 1, 4 ], - 'foobar', - 'foobar' // No selection in view. - ); - } ); - - it( 'should convert backward selection', () => { - test( - [ 1, 3, 'backward' ], - 'foobar', - 'f{oo}bar' - ); - - expect( viewSelection.focus.offset ).to.equal( 1 ); - } ); - } ); - - describe( 'collapsed selection', () => { - let marker; - - it( 'in container', () => { - test( - [ 1, 1 ], - 'foobar', - 'f{}oobar' - ); - } ); - - it( 'in attribute', () => { - test( - [ 3, 3 ], - 'f<$text bold="true">oobar', - 'foo{}bar' - ); - } ); - - it( 'in attribute and marker', () => { - setModelData( model, 'fo<$text bold="true">obar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( - '
foo{}bar
' - ); - } ); - - it( 'in attribute and marker - no attribute', () => { - setModelData( model, 'fo<$text bold="true">obar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - writer.removeSelectionAttribute( 'bold' ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
foo[]bar
' ); - } ); - - it( 'in marker - using highlight descriptor creator', () => { - downcastHelpers.markerToHighlight( { - model: 'marker2', - view: data => ( { classes: data.markerName } ) - } ); - - setModelData( model, 'foobar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker2', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
foo{}bar
' ); - } ); - - it( 'should do nothing if creator return null', () => { - downcastHelpers.markerToHighlight( { - model: 'marker3', - view: () => null - } ); - - setModelData( model, 'foobar' ); - - model.change( writer => { - const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); - marker = writer.addMarker( 'marker3', { range, usingOperation: false } ); - writer.setSelection( modelRoot, 3 ); - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
foo{}bar
' ); - } ); - - // #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements. - it( 'selection with attribute before ui element - no non-ui children', () => { - setModelData( model, '' ); - - // Add two ui elements to view. - viewRoot._appendChild( [ - new ViewUIElement( 'span' ), - new ViewUIElement( 'span' ) - ] ); - - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 0 ] ) ) ); - writer.setSelectionAttribute( 'bold', true ); - } ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
[]
' ); - } ); - - // #1072. - it( 'selection with attribute before ui element - has non-ui children #1', () => { - setModelData( model, 'x' ); - - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); - writer.setSelectionAttribute( 'bold', true ); - } ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - - // Add ui element to view. - const uiElement = new ViewUIElement( 'span' ); - viewRoot._insertChild( 1, uiElement ); - - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
x[]
' ); - } ); - - // #1072. - it( 'selection with attribute before ui element - has non-ui children #2', () => { - setModelData( model, '<$text bold="true">xy' ); - - model.change( writer => { - writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); - writer.setSelectionAttribute( 'bold', true ); - } ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - - // Add ui element to view. - const uiElement = new ViewUIElement( 'span' ); - viewRoot._insertChild( 1, uiElement, writer ); - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - - // Stringify view and check if it is same as expected. - expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) - .to.equal( '
x{}y
' ); - } ); - - it( 'consumes consumable values properly', () => { - // Add callbacks that will fire before default ones. - // This should prevent default callbacks doing anything. - dispatcher.on( 'selection', ( evt, data, conversionApi ) => { - expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; - }, { priority: 'high' } ); - - dispatcher.on( 'attribute:bold', ( evt, data, conversionApi ) => { - expect( conversionApi.consumable.consume( data.item, 'attribute:bold' ) ).to.be.true; - }, { priority: 'high' } ); - - // Similar test case as above. - test( - [ 3, 3 ], - 'f<$text bold="true">oobar', - 'foobar' // No selection in view and no attribute. - ); - } ); - } ); - } ); - - describe( 'clean-up', () => { - describe( 'convertRangeSelection', () => { - it( 'should remove all ranges before adding new range', () => { - test( - [ 0, 2 ], - 'foobar', - '{fo}obar' - ); - - test( - [ 3, 5 ], - 'foobar', - 'foo{ba}r' - ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - } ); - } ); - - describe( 'convertCollapsedSelection', () => { - it( 'should remove all ranges before adding new range', () => { - test( - [ 2, 2 ], - 'foobar', - 'fo{}obar' - ); - - test( - [ 3, 3 ], - 'foobar', - 'foo{}bar' - ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - } ); - } ); - - describe( 'clearAttributes', () => { - it( 'should remove all ranges before adding new range', () => { - test( - [ 3, 3 ], - 'foobar', - 'foo[]bar', - { bold: 'true' } - ); - - view.change( writer => { - const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); - model.change( writer => { - writer.setSelection( modelRange ); - } ); - - dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); - } ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - - const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); - expect( viewString ).to.equal( '
f{}oobar
' ); - } ); - - it( 'should do nothing if the attribute element had been already removed', () => { - test( - [ 3, 3 ], - 'foobar', - 'foo[]bar', - { bold: 'true' } - ); - - view.change( writer => { - // Remove manually. - writer.mergeAttributes( viewSelection.getFirstPosition() ); - - const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); - model.change( writer => { - writer.setSelection( modelRange ); - } ); - - dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); - } ); - - expect( viewSelection.rangeCount ).to.equal( 1 ); - - const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); - expect( viewString ).to.equal( '
f{}oobar
' ); - } ); - - it( 'should clear fake selection', () => { - const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); - - view.change( writer => { - writer.setSelection( modelRange, { fake: true } ); - - dispatcher.convertSelection( docSelection, model.markers, writer ); - } ); - expect( viewSelection.isFake ).to.be.false; - } ); - } ); - } ); - - describe( 'table cell selection converter', () => { - beforeEach( () => { - model.schema.register( 'table', { isLimit: true } ); - model.schema.register( 'tr', { isLimit: true } ); - model.schema.register( 'td', { isLimit: true } ); - - model.schema.extend( 'table', { allowIn: '$root' } ); - model.schema.extend( 'tr', { allowIn: 'table' } ); - model.schema.extend( 'td', { allowIn: 'tr' } ); - model.schema.extend( '$text', { allowIn: 'td' } ); - - const downcastHelpers = new DowncastHelpers( dispatcher ); - - // "Universal" converter to convert table structure. - downcastHelpers.elementToElement( { model: 'table', view: 'table' } ); - downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } ); - downcastHelpers.elementToElement( { model: 'td', view: 'td' } ); - - // Special converter for table cells. - dispatcher.on( 'selection', ( evt, data, conversionApi ) => { - const selection = data.selection; - - if ( !conversionApi.consumable.test( selection, 'selection' ) || selection.isCollapsed ) { - return; - } - - for ( const range of selection.getRanges() ) { - const node = range.start.parent; - - if ( !!node && node.is( 'td' ) ) { - conversionApi.consumable.consume( selection, 'selection' ); - - const viewNode = conversionApi.mapper.toViewElement( node ); - conversionApi.writer.addClass( 'selected', viewNode ); - } - } - }, { priority: 'high' } ); - } ); - - it( 'should not be used to convert selection that is not on table cell', () => { - test( - [ 1, 5 ], - 'f{o<$text bold="true">oba}r', - 'f{ooba}r' - ); - } ); - - it( 'should add a class to the selected table cell', () => { - test( - // table tr#0 td#0 [foo, table tr#0 td#0 bar] - [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 3 ] ], - '
foo
bar
', - '
foo
bar
' - ); - } ); - - it( 'should not be used if selection contains more than just a table cell', () => { - test( - // table tr td#1 f{oo bar, table tr#2 bar] - [ [ 0, 0, 0, 1 ], [ 0, 0, 1, 3 ] ], - '
foobar
', - '[
foobar
]' - ); - } ); - } ); - - // Tests if the selection got correctly converted. - // Because `setData` might use selection converters itself to set the selection, we can't use it - // to set the selection (because then we would test converters using converters). - // Instead, the `test` function expects to be passed `selectionPaths` which is an array containing two numbers or two arrays, - // that are offsets or paths of selection positions in root element. - function test( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) { - // Parse passed `modelInput` string and set it as current model. - setModelData( model, modelInput ); - - // Manually set selection ranges using passed `selectionPaths`. - const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ]; - const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ]; - - const startPos = model.createPositionFromPath( modelRoot, startPath ); - const endPos = model.createPositionFromPath( modelRoot, endPath ); - - const isBackward = selectionPaths[ 2 ] === 'backward'; - model.change( writer => { - writer.setSelection( writer.createRange( startPos, endPos ), { backward: isBackward } ); - - // And add or remove passed attributes. - for ( const key in selectionAttributes ) { - const value = selectionAttributes[ key ]; - - if ( value ) { - writer.setSelectionAttribute( key, value ); - } else { - writer.removeSelectionAttribute( key ); - } - } - } ); - - // Remove view children manually (without firing additional conversion). - viewRoot._removeChildren( 0, viewRoot.childCount ); - - // Convert model to view. - view.change( writer => { - dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); - dispatcher.convertSelection( docSelection, model.markers, 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/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 8be542608..290b032c8 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -20,9 +20,18 @@ import ViewText from '../../src/view/text'; import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import DowncastHelpers, { createViewElementFromHighlightDescriptor } from '../../src/conversion/downcasthelpers'; - -import { stringify } from '../../src/dev-utils/view'; +import DowncastHelpers, { + clearAttributes, convertCollapsedSelection, convertRangeSelection, + createViewElementFromHighlightDescriptor, + insertText +} from '../../src/conversion/downcasthelpers'; + +import Mapper from '../../src/conversion/mapper'; +import DowncastDispatcher from '../../src/conversion/downcastdispatcher'; +import { stringify as stringifyView } from '../../src/dev-utils/view'; +import View from '../../src/view/view'; +import createViewRoot from '../view/_utils/createroot'; +import { setData as setModelData } from '../../src/dev-utils/model'; describe( 'DowncastHelpers', () => { let conversion, model, modelRoot, viewRoot, downcastHelpers, controller; @@ -1443,7 +1452,7 @@ describe( 'DowncastHelpers', () => { } ); function expectResult( string ) { - expect( stringify( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string ); + expect( stringifyView( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string ); } function viewAttributesToString( item ) { @@ -1837,3 +1846,575 @@ describe( 'downcast-converters', () => { } ); } ); } ); + +describe( 'downcast-selection-converters', () => { + let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers; + + beforeEach( () => { + model = new Model(); + modelDoc = model.document; + modelRoot = modelDoc.createRoot(); + docSelection = modelDoc.selection; + + model.schema.extend( '$text', { allowIn: '$root' } ); + + view = new View(); + viewDoc = view.document; + viewRoot = createViewRoot( viewDoc ); + viewSelection = viewDoc.selection; + + mapper = new Mapper(); + mapper.bindElements( modelRoot, viewRoot ); + + dispatcher = new DowncastDispatcher( { mapper, viewSelection } ); + + dispatcher.on( 'insert:$text', insertText() ); + + downcastHelpers = new DowncastHelpers( dispatcher ); + downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); + downcastHelpers.markerToHighlight( { model: 'marker', view: { classes: 'marker' }, converterPriority: 1 } ); + + // Default selection converters. + dispatcher.on( 'selection', clearAttributes(), { priority: 'low' } ); + dispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } ); + dispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + describe( 'default converters', () => { + describe( 'range selection', () => { + it( 'in same container', () => { + test( + [ 1, 4 ], + 'foobar', + 'f{oob}ar' + ); + } ); + + it( 'in same container with unicode characters', () => { + test( + [ 2, 6 ], + 'நிலைக்கு', + 'நி{லைக்}கு' + ); + } ); + + it( 'in same container, over attribute', () => { + test( + [ 1, 5 ], + 'fo<$text bold="true">obar', + 'f{ooba}r' + ); + } ); + + it( 'in same container, next to attribute', () => { + test( + [ 1, 2 ], + 'fo<$text bold="true">obar', + 'f{o}obar' + ); + } ); + + it( 'in same attribute', () => { + test( + [ 2, 4 ], + 'f<$text bold="true">oobar', + 'fo{ob}ar' + ); + } ); + + it( 'in same attribute, selection same as attribute', () => { + test( + [ 2, 4 ], + 'fo<$text bold="true">obar', + 'fo{ob}ar' + ); + } ); + + it( 'starts in text node, ends in attribute #1', () => { + test( + [ 1, 3 ], + 'fo<$text bold="true">obar', + 'f{oo}bar' + ); + } ); + + it( 'starts in text node, ends in attribute #2', () => { + test( + [ 1, 4 ], + 'fo<$text bold="true">obar', + 'f{oob}ar' + ); + } ); + + it( 'starts in attribute, ends in text node', () => { + test( + [ 3, 5 ], + 'fo<$text bold="true">obar', + 'foo{ba}r' + ); + } ); + + it( 'consumes consumable values properly', () => { + // Add callback that will fire before default ones. + // This should prevent default callback doing anything. + dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; + }, { priority: 'high' } ); + + // Similar test case as the first in this suite. + test( + [ 1, 4 ], + 'foobar', + 'foobar' // No selection in view. + ); + } ); + + it( 'should convert backward selection', () => { + test( + [ 1, 3, 'backward' ], + 'foobar', + 'f{oo}bar' + ); + + expect( viewSelection.focus.offset ).to.equal( 1 ); + } ); + } ); + + describe( 'collapsed selection', () => { + let marker; + + it( 'in container', () => { + test( + [ 1, 1 ], + 'foobar', + 'f{}oobar' + ); + } ); + + it( 'in attribute', () => { + test( + [ 3, 3 ], + 'f<$text bold="true">oobar', + 'foo{}bar' + ); + } ); + + it( 'in attribute and marker', () => { + setModelData( model, 'fo<$text bold="true">obar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( + '
foo{}bar
' + ); + } ); + + it( 'in attribute and marker - no attribute', () => { + setModelData( model, 'fo<$text bold="true">obar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + writer.removeSelectionAttribute( 'bold' ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
foo[]bar
' ); + } ); + + it( 'in marker - using highlight descriptor creator', () => { + downcastHelpers.markerToHighlight( { + model: 'marker2', + view: data => ( { classes: data.markerName } ) + } ); + + setModelData( model, 'foobar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker2', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
foo{}bar
' ); + } ); + + it( 'should do nothing if creator return null', () => { + downcastHelpers.markerToHighlight( { + model: 'marker3', + view: () => null + } ); + + setModelData( model, 'foobar' ); + + model.change( writer => { + const range = writer.createRange( writer.createPositionAt( modelRoot, 1 ), writer.createPositionAt( modelRoot, 5 ) ); + marker = writer.addMarker( 'marker3', { range, usingOperation: false } ); + writer.setSelection( modelRoot, 3 ); + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertMarkerAdd( marker.name, marker.getRange(), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
foo{}bar
' ); + } ); + + // #1072 - if the container has only ui elements, collapsed selection attribute should be rendered after those ui elements. + it( 'selection with attribute before ui element - no non-ui children', () => { + setModelData( model, '' ); + + // Add two ui elements to view. + viewRoot._appendChild( [ + new ViewUIElement( 'span' ), + new ViewUIElement( 'span' ) + ] ); + + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 0 ] ) ) ); + writer.setSelectionAttribute( 'bold', true ); + } ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
[]
' ); + } ); + + // #1072. + it( 'selection with attribute before ui element - has non-ui children #1', () => { + setModelData( model, 'x' ); + + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); + writer.setSelectionAttribute( 'bold', true ); + } ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + + // Add ui element to view. + const uiElement = new ViewUIElement( 'span' ); + viewRoot._insertChild( 1, uiElement ); + + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
x[]
' ); + } ); + + // #1072. + it( 'selection with attribute before ui element - has non-ui children #2', () => { + setModelData( model, '<$text bold="true">xy' ); + + model.change( writer => { + writer.setSelection( writer.createRange( writer.createPositionFromPath( modelRoot, [ 1 ] ) ) ); + writer.setSelectionAttribute( 'bold', true ); + } ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + + // Add ui element to view. + const uiElement = new ViewUIElement( 'span' ); + viewRoot._insertChild( 1, uiElement, writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ) + .to.equal( '
x{}y
' ); + } ); + + it( 'consumes consumable values properly', () => { + // Add callbacks that will fire before default ones. + // This should prevent default callbacks doing anything. + dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + expect( conversionApi.consumable.consume( data.selection, 'selection' ) ).to.be.true; + }, { priority: 'high' } ); + + dispatcher.on( 'attribute:bold', ( evt, data, conversionApi ) => { + expect( conversionApi.consumable.consume( data.item, 'attribute:bold' ) ).to.be.true; + }, { priority: 'high' } ); + + // Similar test case as above. + test( + [ 3, 3 ], + 'f<$text bold="true">oobar', + 'foobar' // No selection in view and no attribute. + ); + } ); + } ); + } ); + + describe( 'clean-up', () => { + describe( 'convertRangeSelection', () => { + it( 'should remove all ranges before adding new range', () => { + test( + [ 0, 2 ], + 'foobar', + '{fo}obar' + ); + + test( + [ 3, 5 ], + 'foobar', + 'foo{ba}r' + ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + } ); + } ); + + describe( 'convertCollapsedSelection', () => { + it( 'should remove all ranges before adding new range', () => { + test( + [ 2, 2 ], + 'foobar', + 'fo{}obar' + ); + + test( + [ 3, 3 ], + 'foobar', + 'foo{}bar' + ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + } ); + } ); + + describe( 'clearAttributes', () => { + it( 'should remove all ranges before adding new range', () => { + test( + [ 3, 3 ], + 'foobar', + 'foo[]bar', + { bold: 'true' } + ); + + view.change( writer => { + const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); + model.change( writer => { + writer.setSelection( modelRange ); + } ); + + dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); + } ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + + const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); + expect( viewString ).to.equal( '
f{}oobar
' ); + } ); + + it( 'should do nothing if the attribute element had been already removed', () => { + test( + [ 3, 3 ], + 'foobar', + 'foo[]bar', + { bold: 'true' } + ); + + view.change( writer => { + // Remove manually. + writer.mergeAttributes( viewSelection.getFirstPosition() ); + + const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); + model.change( writer => { + writer.setSelection( modelRange ); + } ); + + dispatcher.convertSelection( modelDoc.selection, model.markers, writer ); + } ); + + expect( viewSelection.rangeCount ).to.equal( 1 ); + + const viewString = stringifyView( viewRoot, viewSelection, { showType: false } ); + expect( viewString ).to.equal( '
f{}oobar
' ); + } ); + + it( 'should clear fake selection', () => { + const modelRange = model.createRange( model.createPositionAt( modelRoot, 1 ), model.createPositionAt( modelRoot, 1 ) ); + + view.change( writer => { + writer.setSelection( modelRange, { fake: true } ); + + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + expect( viewSelection.isFake ).to.be.false; + } ); + } ); + } ); + + describe( 'table cell selection converter', () => { + beforeEach( () => { + model.schema.register( 'table', { isLimit: true } ); + model.schema.register( 'tr', { isLimit: true } ); + model.schema.register( 'td', { isLimit: true } ); + + model.schema.extend( 'table', { allowIn: '$root' } ); + model.schema.extend( 'tr', { allowIn: 'table' } ); + model.schema.extend( 'td', { allowIn: 'tr' } ); + model.schema.extend( '$text', { allowIn: 'td' } ); + + const downcastHelpers = new DowncastHelpers( dispatcher ); + + // "Universal" converter to convert table structure. + downcastHelpers.elementToElement( { model: 'table', view: 'table' } ); + downcastHelpers.elementToElement( { model: 'tr', view: 'tr' } ); + downcastHelpers.elementToElement( { model: 'td', view: 'td' } ); + + // Special converter for table cells. + dispatcher.on( 'selection', ( evt, data, conversionApi ) => { + const selection = data.selection; + + if ( !conversionApi.consumable.test( selection, 'selection' ) || selection.isCollapsed ) { + return; + } + + for ( const range of selection.getRanges() ) { + const node = range.start.parent; + + if ( !!node && node.is( 'td' ) ) { + conversionApi.consumable.consume( selection, 'selection' ); + + const viewNode = conversionApi.mapper.toViewElement( node ); + conversionApi.writer.addClass( 'selected', viewNode ); + } + } + }, { priority: 'high' } ); + } ); + + it( 'should not be used to convert selection that is not on table cell', () => { + test( + [ 1, 5 ], + 'f{o<$text bold="true">oba}r', + 'f{ooba}r' + ); + } ); + + it( 'should add a class to the selected table cell', () => { + test( + // table tr#0 td#0 [foo, table tr#0 td#0 bar] + [ [ 0, 0, 0, 0 ], [ 0, 0, 0, 3 ] ], + '
foo
bar
', + '
foo
bar
' + ); + } ); + + it( 'should not be used if selection contains more than just a table cell', () => { + test( + // table tr td#1 f{oo bar, table tr#2 bar] + [ [ 0, 0, 0, 1 ], [ 0, 0, 1, 3 ] ], + '
foobar
', + '[
foobar
]' + ); + } ); + } ); + + // Tests if the selection got correctly converted. + // Because `setData` might use selection converters itself to set the selection, we can't use it + // to set the selection (because then we would test converters using converters). + // Instead, the `test` function expects to be passed `selectionPaths` which is an array containing two numbers or two arrays, + // that are offsets or paths of selection positions in root element. + function test( selectionPaths, modelInput, expectedView, selectionAttributes = {} ) { + // Parse passed `modelInput` string and set it as current model. + setModelData( model, modelInput ); + + // Manually set selection ranges using passed `selectionPaths`. + const startPath = typeof selectionPaths[ 0 ] == 'number' ? [ selectionPaths[ 0 ] ] : selectionPaths[ 0 ]; + const endPath = typeof selectionPaths[ 1 ] == 'number' ? [ selectionPaths[ 1 ] ] : selectionPaths[ 1 ]; + + const startPos = model.createPositionFromPath( modelRoot, startPath ); + const endPos = model.createPositionFromPath( modelRoot, endPath ); + + const isBackward = selectionPaths[ 2 ] === 'backward'; + model.change( writer => { + writer.setSelection( writer.createRange( startPos, endPos ), { backward: isBackward } ); + + // And add or remove passed attributes. + for ( const key in selectionAttributes ) { + const value = selectionAttributes[ key ]; + + if ( value ) { + writer.setSelectionAttribute( key, value ); + } else { + writer.removeSelectionAttribute( key ); + } + } + } ); + + // Remove view children manually (without firing additional conversion). + viewRoot._removeChildren( 0, viewRoot.childCount ); + + // Convert model to view. + view.change( writer => { + dispatcher.convertInsert( model.createRangeIn( modelRoot ), writer ); + dispatcher.convertSelection( docSelection, model.markers, writer ); + } ); + + // Stringify view and check if it is same as expected. + expect( stringifyView( viewRoot, viewSelection, { showType: false } ) ).to.equal( '
' + expectedView + '
' ); + } +} ); + From 721cb8cc64d5eafa6adbea993dc1bb88c17f5459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:56:27 +0100 Subject: [PATCH 3/8] Update documentation links to downcast selection converters. --- src/conversion/downcasthelpers.js | 4 ++-- src/conversion/modelconsumable.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/conversion/downcasthelpers.js b/src/conversion/downcasthelpers.js index 734486938..51215ef00 100644 --- a/src/conversion/downcasthelpers.js +++ b/src/conversion/downcasthelpers.js @@ -470,7 +470,7 @@ export function convertRangeSelection() { * converted, broken attributes might be merged again, or the position where the selection is may be wrapped * with different, appropriate attribute elements. * - * See also {@link module:engine/conversion/downcast-selection-converters~clearAttributes} which does a clean-up + * See also {@link module:engine/conversion/downcasthelpers~clearAttributes} which does a clean-up * by merging attributes. * * @returns {Function} Selection converter. @@ -515,7 +515,7 @@ export function convertCollapsedSelection() { * * modelDispatcher.on( 'selection', clearAttributes() ); * - * See {@link module:engine/conversion/downcast-selection-converters~convertCollapsedSelection} + * See {@link module:engine/conversion/downcasthelpers~convertCollapsedSelection} * which does the opposite by breaking attributes in the selection position. * * @returns {Function} Selection converter. diff --git a/src/conversion/modelconsumable.js b/src/conversion/modelconsumable.js index 50c9cf89a..6f0957b91 100644 --- a/src/conversion/modelconsumable.js +++ b/src/conversion/modelconsumable.js @@ -29,7 +29,7 @@ import TextProxy from '../model/textproxy'; * {@link module:engine/conversion/modelconsumable~ModelConsumable#add add method} directly. * However, it is important to understand how consumable values can be * {@link module:engine/conversion/modelconsumable~ModelConsumable#consume consumed}. - * See {@link module:engine/conversion/downcast-selection-converters default downcast converters} for more information. + * See {@link module:engine/conversion/downcasthelpers default downcast converters} for more information. * * Keep in mind, that one conversion event may have multiple callbacks (converters) attached to it. Each of those is * able to convert one or more parts of the model. However, when one of those callbacks actually converts From 4a2b6dd04c223ceb2887d67a3643cb7aa5037f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 15:57:21 +0100 Subject: [PATCH 4/8] Remove redundant code from DowncastHelpers tests. --- tests/conversion/downcasthelpers.js | 106 ++++++++++------------------ 1 file changed, 36 insertions(+), 70 deletions(-) diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index 290b032c8..d12f21c68 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -1454,42 +1454,9 @@ describe( 'DowncastHelpers', () => { function expectResult( string ) { expect( stringifyView( viewRoot, null, { ignoreRoot: true } ) ).to.equal( string ); } - - function viewAttributesToString( item ) { - let result = ''; - - for ( const key of item.getAttributeKeys() ) { - const value = item.getAttribute( key ); - - if ( value ) { - result += ' ' + key + '="' + value + '"'; - } - } - - return result; - } - - function viewToString( item ) { - let result = ''; - - if ( item instanceof ViewText ) { - result = item.data; - } else { - // ViewElement or ViewDocumentFragment. - for ( const child of item.getChildren() ) { - result += viewToString( child ); - } - - if ( item instanceof ViewElement ) { - result = '<' + item.name + viewAttributesToString( item ) + '>' + result + ''; - } - } - - return result; - } } ); -describe( 'downcast-converters', () => { +describe( 'downcast converters', () => { let dispatcher, modelDoc, modelRoot, viewRoot, controller, modelRootStart, model; beforeEach( () => { @@ -1511,40 +1478,7 @@ describe( 'downcast-converters', () => { modelRootStart = model.createPositionAt( modelRoot, 0 ); } ); - function viewAttributesToString( item ) { - let result = ''; - - for ( const key of item.getAttributeKeys() ) { - const value = item.getAttribute( key ); - - if ( value ) { - result += ' ' + key + '="' + value + '"'; - } - } - - return result; - } - - function viewToString( item ) { - let result = ''; - - if ( item instanceof ViewText ) { - result = item.data; - } else { - // ViewElement or ViewDocumentFragment. - for ( const child of item.getChildren() ) { - result += viewToString( child ); - } - - if ( item instanceof ViewElement ) { - result = '<' + item.name + viewAttributesToString( item ) + '>' + result + ''; - } - } - - return result; - } - - describe( 'insertText', () => { + describe( 'insertText()', () => { it( 'should downcast text', () => { model.change( writer => { writer.insert( new ModelText( 'foobar' ), modelRootStart ); @@ -1575,7 +1509,7 @@ describe( 'downcast-converters', () => { } ); // Remove converter is by default already added in `EditingController` instance. - describe( 'remove', () => { + describe( 'remove()', () => { it( 'should remove items from view accordingly to changes in model #1', () => { const modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foobar' ) ); @@ -1847,7 +1781,7 @@ describe( 'downcast-converters', () => { } ); } ); -describe( 'downcast-selection-converters', () => { +describe( 'downcast selection converters', () => { let dispatcher, mapper, model, view, modelDoc, modelRoot, docSelection, viewDoc, viewRoot, viewSelection, downcastHelpers; beforeEach( () => { @@ -2418,3 +2352,35 @@ describe( 'downcast-selection-converters', () => { } } ); +function viewToString( item ) { + let result = ''; + + if ( item instanceof ViewText ) { + result = item.data; + } else { + // ViewElement or ViewDocumentFragment. + for ( const child of item.getChildren() ) { + result += viewToString( child ); + } + + if ( item instanceof ViewElement ) { + result = '<' + item.name + viewAttributesToString( item ) + '>' + result + ''; + } + } + + return result; +} + +function viewAttributesToString( item ) { + let result = ''; + + for ( const key of item.getAttributeKeys() ) { + const value = item.getAttribute( key ); + + if ( value ) { + result += ' ' + key + '="' + value + '"'; + } + } + + return result; +} From f16eebaa087999232aa86631824840988ad6774a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:01:10 +0100 Subject: [PATCH 5/8] Move upcast conversion helpers for selection to upcasthelpers.js. --- src/controller/editingcontroller.js | 2 +- src/conversion/upcast-selection-converters.js | 48 ------------------- src/conversion/upcasthelpers.js | 36 ++++++++++++++ .../conversion/upcast-selection-converters.js | 2 +- 4 files changed, 38 insertions(+), 50 deletions(-) delete mode 100644 src/conversion/upcast-selection-converters.js diff --git a/src/controller/editingcontroller.js b/src/controller/editingcontroller.js index d7a7ed732..cec69c9a1 100644 --- a/src/controller/editingcontroller.js +++ b/src/controller/editingcontroller.js @@ -12,10 +12,10 @@ import View from '../view/view'; import Mapper from '../conversion/mapper'; import DowncastDispatcher from '../conversion/downcastdispatcher'; import { clearAttributes, convertCollapsedSelection, convertRangeSelection, insertText, remove } from '../conversion/downcasthelpers'; -import { convertSelectionChange } from '../conversion/upcast-selection-converters'; import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; +import { convertSelectionChange } from '../conversion/upcasthelpers'; /** * Controller for the editing pipeline. The editing pipeline controls {@link ~EditingController#model model} rendering, diff --git a/src/conversion/upcast-selection-converters.js b/src/conversion/upcast-selection-converters.js deleted file mode 100644 index 5ff080497..000000000 --- a/src/conversion/upcast-selection-converters.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * Contains {@link module:engine/view/documentselection~DocumentSelection view selection} - * to {@link module:engine/model/selection~Selection model selection} conversion helpers. - * - * @module engine/conversion/upcast-selection-converters - */ - -import ModelSelection from '../model/selection'; - -/** - * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection - * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event - * and sets in on the {@link module:engine/model/document~Document#selection model}. - * - * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model - * conversion mechanism, the callback should be set directly on view document. - * - * view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) ); - * - * @param {module:engine/model/model~Model} model Data model. - * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper. - * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function. - */ -export function convertSelectionChange( model, mapper ) { - return ( evt, data ) => { - const viewSelection = data.newSelection; - const modelSelection = new ModelSelection(); - - const ranges = []; - - for ( const viewRange of viewSelection.getRanges() ) { - ranges.push( mapper.toModelRange( viewRange ) ); - } - - modelSelection.setTo( ranges, { backward: viewSelection.isBackward } ); - - if ( !modelSelection.isEqual( model.document.selection ) ) { - model.change( writer => { - writer.setSelection( modelSelection ); - } ); - } - }; -} diff --git a/src/conversion/upcasthelpers.js b/src/conversion/upcasthelpers.js index 5c7f38ce1..34657753e 100644 --- a/src/conversion/upcasthelpers.js +++ b/src/conversion/upcasthelpers.js @@ -8,6 +8,7 @@ import ModelRange from '../model/range'; import { ConversionHelpers } from './conversion'; import { cloneDeep } from 'lodash-es'; +import ModelSelection from '../model/selection'; /** * Contains {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for @@ -352,6 +353,41 @@ export function convertText() { }; } +/** + * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection + * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event + * and sets in on the {@link module:engine/model/document~Document#selection model}. + * + * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model + * conversion mechanism, the callback should be set directly on view document. + * + * view.document.on( 'selectionChange', convertSelectionChange( modelDocument, mapper ) ); + * + * @param {module:engine/model/model~Model} model Data model. + * @param {module:engine/conversion/mapper~Mapper} mapper Conversion mapper. + * @returns {Function} {@link module:engine/view/document~Document#event:selectionChange} callback function. + */ +export function convertSelectionChange( model, mapper ) { + return ( evt, data ) => { + const viewSelection = data.newSelection; + const modelSelection = new ModelSelection(); + + const ranges = []; + + for ( const viewRange of viewSelection.getRanges() ) { + ranges.push( mapper.toModelRange( viewRange ) ); + } + + modelSelection.setTo( ranges, { backward: viewSelection.isBackward } ); + + if ( !modelSelection.isEqual( model.document.selection ) ) { + model.change( writer => { + writer.setSelection( modelSelection ); + } ); + } + }; +} + // View element to model element conversion helper. // // See {@link ~UpcastHelpers#elementToElement `.elementToElement()` upcast helper} for examples. diff --git a/tests/conversion/upcast-selection-converters.js b/tests/conversion/upcast-selection-converters.js index 18a95d92f..fbe75b3d6 100644 --- a/tests/conversion/upcast-selection-converters.js +++ b/tests/conversion/upcast-selection-converters.js @@ -11,10 +11,10 @@ import createViewRoot from '../view/_utils/createroot'; import Model from '../../src/model/model'; import Mapper from '../../src/conversion/mapper'; -import { convertSelectionChange } from '../../src/conversion/upcast-selection-converters'; import { setData as modelSetData, getData as modelGetData } from '../../src/dev-utils/model'; import { setData as viewSetData } from '../../src/dev-utils/view'; +import { convertSelectionChange } from '../../src/conversion/upcasthelpers'; describe( 'convertSelectionChange', () => { let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; From 067934d8f3ca33871d0337247cd8bb6cfd402614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:03:25 +0100 Subject: [PATCH 6/8] Remove upcast-selection-converters.js. --- .../conversion/upcast-selection-converters.js | 131 ------------------ tests/conversion/upcasthelpers.js | 124 ++++++++++++++++- 2 files changed, 122 insertions(+), 133 deletions(-) delete mode 100644 tests/conversion/upcast-selection-converters.js diff --git a/tests/conversion/upcast-selection-converters.js b/tests/conversion/upcast-selection-converters.js deleted file mode 100644 index fbe75b3d6..000000000 --- a/tests/conversion/upcast-selection-converters.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import View from '../../src/view/view'; -import ViewSelection from '../../src/view/selection'; -import ViewRange from '../../src/view/range'; -import createViewRoot from '../view/_utils/createroot'; - -import Model from '../../src/model/model'; - -import Mapper from '../../src/conversion/mapper'; - -import { setData as modelSetData, getData as modelGetData } from '../../src/dev-utils/model'; -import { setData as viewSetData } from '../../src/dev-utils/view'; -import { convertSelectionChange } from '../../src/conversion/upcasthelpers'; - -describe( 'convertSelectionChange', () => { - let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; - - beforeEach( () => { - model = new Model(); - modelRoot = model.document.createRoot(); - model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - - modelSetData( model, 'foobar' ); - - view = new View(); - viewDocument = view.document; - viewRoot = createViewRoot( viewDocument, 'div', 'main' ); - - viewSetData( view, '

foo

bar

' ); - - mapper = new Mapper(); - mapper.bindElements( modelRoot, viewRoot ); - mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); - mapper.bindElements( modelRoot.getChild( 1 ), viewRoot.getChild( 1 ) ); - - convertSelection = convertSelectionChange( model, mapper ); - } ); - - afterEach( () => { - view.destroy(); - } ); - - it( 'should convert collapsed selection', () => { - const viewSelection = new ViewSelection(); - viewSelection.setTo( ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equals( 'f[]oobar' ); - expect( modelGetData( model ) ).to.equal( 'f[]oobar' ); - } ); - - it( 'should support unicode', () => { - modelSetData( model, 'நிலைக்கு' ); - viewSetData( view, '

நிலைக்கு

' ); - - // Re-bind elements that were just re-set. - mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); - - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 ) - ] ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equal( 'நி[லைக்]கு' ); - } ); - - it( 'should convert multi ranges selection', () => { - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) - ] ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equal( - 'f[o]ob[a]r' ); - - const ranges = Array.from( model.document.selection.getRanges() ); - expect( ranges.length ).to.equal( 2 ); - - expect( ranges[ 0 ].start.parent ).to.equal( modelRoot.getChild( 0 ) ); - expect( ranges[ 0 ].start.offset ).to.equal( 1 ); - expect( ranges[ 0 ].end.parent ).to.equal( modelRoot.getChild( 0 ) ); - expect( ranges[ 0 ].end.offset ).to.equal( 2 ); - - expect( ranges[ 1 ].start.parent ).to.equal( modelRoot.getChild( 1 ) ); - expect( ranges[ 1 ].start.offset ).to.equal( 1 ); - expect( ranges[ 1 ].end.parent ).to.equal( modelRoot.getChild( 1 ) ); - expect( ranges[ 1 ].end.offset ).to.equal( 2 ); - } ); - - it( 'should convert reverse selection', () => { - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) - ], { backward: true } ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( modelGetData( model ) ).to.equal( 'f[o]ob[a]r' ); - expect( model.document.selection.isBackward ).to.true; - } ); - - it( 'should not enqueue changes if selection has not changed', () => { - const viewSelection = new ViewSelection( [ - ViewRange._createFromParentsAndOffsets( - viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) - ] ); - - convertSelection( null, { newSelection: viewSelection } ); - - const spy = sinon.spy(); - - model.on( 'change', spy ); - - convertSelection( null, { newSelection: viewSelection } ); - - expect( spy.called ).to.be.false; - } ); -} ); diff --git a/tests/conversion/upcasthelpers.js b/tests/conversion/upcasthelpers.js index c7951e41f..12f017605 100644 --- a/tests/conversion/upcasthelpers.js +++ b/tests/conversion/upcasthelpers.js @@ -19,9 +19,15 @@ import ModelText from '../../src/model/text'; import ModelRange from '../../src/model/range'; import ModelPosition from '../../src/model/position'; -import UpcastHelpers, { convertToModelFragment, convertText } from '../../src/conversion/upcasthelpers'; +import UpcastHelpers, { convertToModelFragment, convertText, convertSelectionChange } from '../../src/conversion/upcasthelpers'; -import { stringify } from '../../src/dev-utils/model'; +import { getData as modelGetData, setData as modelSetData, stringify } from '../../src/dev-utils/model'; +import View from '../../src/view/view'; +import createViewRoot from '../view/_utils/createroot'; +import { setData as viewSetData } from '../../src/dev-utils/view'; +import Mapper from '../../src/conversion/mapper'; +import ViewSelection from '../../src/view/selection'; +import ViewRange from '../../src/view/range'; describe( 'UpcastHelpers', () => { let upcastDispatcher, model, schema, conversion, upcastHelpers; @@ -834,4 +840,118 @@ describe( 'upcast-converters', () => { sinon.assert.calledTwice( spy ); } ); } ); + + describe( 'convertSelectionChange', () => { + let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; + + beforeEach( () => { + model = new Model(); + modelRoot = model.document.createRoot(); + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + + modelSetData( model, 'foobar' ); + + view = new View(); + viewDocument = view.document; + viewRoot = createViewRoot( viewDocument, 'div', 'main' ); + + viewSetData( view, '

foo

bar

' ); + + mapper = new Mapper(); + mapper.bindElements( modelRoot, viewRoot ); + mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); + mapper.bindElements( modelRoot.getChild( 1 ), viewRoot.getChild( 1 ) ); + + convertSelection = convertSelectionChange( model, mapper ); + } ); + + afterEach( () => { + view.destroy(); + } ); + + it( 'should convert collapsed selection', () => { + const viewSelection = new ViewSelection(); + viewSelection.setTo( ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equals( 'f[]oobar' ); + expect( modelGetData( model ) ).to.equal( 'f[]oobar' ); + } ); + + it( 'should support unicode', () => { + modelSetData( model, 'நிலைக்கு' ); + viewSetData( view, '

நிலைக்கு

' ); + + // Re-bind elements that were just re-set. + mapper.bindElements( modelRoot.getChild( 0 ), viewRoot.getChild( 0 ) ); + + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( viewRoot.getChild( 0 ).getChild( 0 ), 2, viewRoot.getChild( 0 ).getChild( 0 ), 6 ) + ] ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equal( 'நி[லைக்]கு' ); + } ); + + it( 'should convert multi ranges selection', () => { + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) + ] ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equal( + 'f[o]ob[a]r' ); + + const ranges = Array.from( model.document.selection.getRanges() ); + expect( ranges.length ).to.equal( 2 ); + + expect( ranges[ 0 ].start.parent ).to.equal( modelRoot.getChild( 0 ) ); + expect( ranges[ 0 ].start.offset ).to.equal( 1 ); + expect( ranges[ 0 ].end.parent ).to.equal( modelRoot.getChild( 0 ) ); + expect( ranges[ 0 ].end.offset ).to.equal( 2 ); + + expect( ranges[ 1 ].start.parent ).to.equal( modelRoot.getChild( 1 ) ); + expect( ranges[ 1 ].start.offset ).to.equal( 1 ); + expect( ranges[ 1 ].end.parent ).to.equal( modelRoot.getChild( 1 ) ); + expect( ranges[ 1 ].end.offset ).to.equal( 2 ); + } ); + + it( 'should convert reverse selection', () => { + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 2 ), + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 1 ).getChild( 0 ), 1, viewRoot.getChild( 1 ).getChild( 0 ), 2 ) + ], { backward: true } ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( modelGetData( model ) ).to.equal( 'f[o]ob[a]r' ); + expect( model.document.selection.isBackward ).to.true; + } ); + + it( 'should not enqueue changes if selection has not changed', () => { + const viewSelection = new ViewSelection( [ + ViewRange._createFromParentsAndOffsets( + viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) + ] ); + + convertSelection( null, { newSelection: viewSelection } ); + + const spy = sinon.spy(); + + model.on( 'change', spy ); + + convertSelection( null, { newSelection: viewSelection } ); + + expect( spy.called ).to.be.false; + } ); + } ); } ); From 9c9a4c969ea808b02ba33d9b4dee8b49c8f7e6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:12:31 +0100 Subject: [PATCH 7/8] Fix code style of imports in downcasthelepers tests. --- tests/conversion/downcasthelpers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/conversion/downcasthelpers.js b/tests/conversion/downcasthelpers.js index d12f21c68..8b88a76b4 100644 --- a/tests/conversion/downcasthelpers.js +++ b/tests/conversion/downcasthelpers.js @@ -21,7 +21,9 @@ import log from '@ckeditor/ckeditor5-utils/src/log'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import DowncastHelpers, { - clearAttributes, convertCollapsedSelection, convertRangeSelection, + clearAttributes, + convertCollapsedSelection, + convertRangeSelection, createViewElementFromHighlightDescriptor, insertText } from '../../src/conversion/downcasthelpers'; From 37a981f26770e67446ccdd49cc1a8d33de7a1081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 2 Jan 2019 16:12:52 +0100 Subject: [PATCH 8/8] Fix code style of test names in upcasthelepers tests. --- tests/conversion/upcasthelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conversion/upcasthelpers.js b/tests/conversion/upcasthelpers.js index 12f017605..00f72e660 100644 --- a/tests/conversion/upcasthelpers.js +++ b/tests/conversion/upcasthelpers.js @@ -841,7 +841,7 @@ describe( 'upcast-converters', () => { } ); } ); - describe( 'convertSelectionChange', () => { + describe( 'convertSelectionChange()', () => { let model, view, viewDocument, mapper, convertSelection, modelRoot, viewRoot; beforeEach( () => {