/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import DowncastWriter from '../../../src/view/downcastwriter';
import { stringify, parse } from '../../../src/dev-utils/view';
import ContainerElement from '../../../src/view/containerelement';
import AttributeElement from '../../../src/view/attributeelement';
import RootEditableElement from '../../../src/view/rooteditableelement';
import EmptyElement from '../../../src/view/emptyelement';
import UIElement from '../../../src/view/uielement';
import Range from '../../../src/view/range';
import Position from '../../../src/view/position';
import Document from '../../../src/view/document';
import Mapper from '../../../src/conversion/mapper';
import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
import { StylesProcessor } from '../../../src/view/stylesmap';
describe( 'DowncastWriter', () => {
describe( 'move()', () => {
let writer, document;
// 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 testMove( 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( () => {
document = new Document( new StylesProcessor() );
writer = new DowncastWriter( document );
} );
it( 'should move single text node', () => {
testMove(
'[foobar]',
'[]',
'',
'[foobar]'
);
} );
it( 'should not leave empty text nodes', () => {
testMove(
'{foobar}',
'[]',
'',
'[foobar]'
);
} );
it( 'should move part of the text node', () => {
testMove(
'f{oob}ar',
'[]',
'far',
'[oob]'
);
} );
it( 'should support unicode', () => {
testMove(
'நி{லை}க்கு',
'நி{}கு',
'நிக்கு',
'நி{லை}கு'
);
} );
it( 'should move parts of nodes', () => {
testMove(
'f{ooba}r',
'[]qux',
'fr',
'[ooba}qux'
);
} );
it( 'should merge after moving #1', () => {
testMove(
'' +
'foo[bar]bazqux' +
'',
'foo{}bazqux',
'foobazqux',
'' +
'foo[bar]bazqux' +
''
);
} );
it( 'should merge after moving #2', () => {
testMove(
'' +
'fo{obarba}zqux' +
'',
'fo{}zqux',
'fozqux',
'' +
'fo{obarba}zqux' +
''
);
} );
it( 'should move part of the text node in document fragment', () => {
testMove( 'fo{ob}ar', 'fo{}ar', 'foar', 'fo{ob}ar' );
} );
it( 'should correctly move text nodes inside same parent', () => {
const { view, selection } = parse( '[a]bc' );
const newRange = writer.move( selection.getFirstRange(), Position._createAt( view, 2 ) );
const expectedView = 'b[a}c';
expect( stringify( view, newRange, { showType: true } ) ).to.equal( expectedView );
} );
it( 'should correctly move text nodes inside same container', () => {
const { view, selection } = parse(
'a{bxxc}dyy'
);
const viewText = view.getChild( 3 );
const newRange = writer.move( selection.getFirstRange(), Position._createAt( viewText, 1 ) );
expect( stringify( view, newRange, { showType: true } ) ).to.equal(
'ady[bxxc]y'
);
} );
it( 'should move EmptyElement', () => {
testMove(
'foo[]bar',
'baz{}quix',
'foobar',
'baz[]quix'
);
} );
it( 'should throw if trying to move to EmptyElement', () => {
const srcAttribute = new AttributeElement( document, 'b' );
const srcContainer = new ContainerElement( document, 'p', null, srcAttribute );
const srcRange = Range._createFromParentsAndOffsets( srcContainer, 0, srcContainer, 1 );
const dstEmpty = new EmptyElement( document, 'img' );
new ContainerElement( document, 'p', null, dstEmpty ); // eslint-disable-line no-new
const dstPosition = new Position( dstEmpty, 0 );
expectToThrowCKEditorError( () => {
writer.move( srcRange, dstPosition );
}, 'view-writer-cannot-break-empty-element', writer );
} );
it( 'should move UIElement', () => {
testMove(
'foo[]bar',
'baz{}quix',
'foobar',
'baz[]quix'
);
} );
it( 'should throw if trying to move to UIElement', () => {
const srcAttribute = new AttributeElement( document, 'b' );
const srcContainer = new ContainerElement( document, 'p', null, srcAttribute );
const srcRange = Range._createFromParentsAndOffsets( srcContainer, 0, srcContainer, 1 );
const dstUI = new UIElement( document, 'span' );
new ContainerElement( document, 'p', null, dstUI ); // eslint-disable-line no-new
const dstPosition = new Position( dstUI, 0 );
expectToThrowCKEditorError( () => {
writer.move( srcRange, dstPosition );
}, 'view-writer-cannot-break-ui-element', writer );
} );
it( 'should not break marker mappings if marker element was split and the original element was removed', () => {
const mapper = new Mapper();
const srcContainer = new ContainerElement( document, 'p' );
const dstContainer = new ContainerElement( document, 'p' );
const root = new RootEditableElement( document, 'div' );
root._appendChild( [ srcContainer, dstContainer ] );
const attrElemA = new AttributeElement( document, 'span' );
attrElemA._id = 'foo';
const attrElemB = new AttributeElement( document, 'span' );
attrElemB._id = 'foo';
writer.insert( new Position( srcContainer, 0 ), [ attrElemA, attrElemB ] );
mapper.bindElementToMarker( attrElemA, 'foo' );
expect( mapper.markerNameToElements( 'foo' ).size ).to.equal( 2 );
writer.remove( writer.createRangeOn( attrElemA ) );
expect( mapper.markerNameToElements( 'foo' ).size ).to.equal( 1 );
writer.move( writer.createRangeOn( attrElemB ), new Position( dstContainer, 0 ) );
expect( mapper.markerNameToElements( 'foo' ).size ).to.equal( 1 );
} );
} );
} );