/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* bender-tags: model, composer */
import Document from '/ckeditor5/engine/model/document.js';
import deleteContents from '/ckeditor5/engine/model/composer/deletecontents.js';
import { setData, getData } from '/ckeditor5/engine/dev-utils/model.js';
describe( 'Delete utils', () => {
let doc;
describe( 'deleteContents', () => {
describe( 'in simple scenarios', () => {
beforeEach( () => {
doc = new Document();
doc.createRoot();
const schema = doc.schema;
schema.registerItem( 'image', '$inline' );
schema.allow( { name: '$text', inside: '$root' } );
schema.allow( { name: 'image', inside: '$root' } );
} );
test(
'does nothing on collapsed selection',
'f[]oo',
'f[]oo'
);
test(
'deletes single character',
'f[o]o',
'f[]o'
);
it( 'deletes single character (backward selection)' , () => {
setData( doc, 'f[o]o', { lastRangeBackward: true } );
deleteContents( doc.batch(), doc.selection );
expect( getData( doc ) ).to.equal( 'f[]o' );
} );
test(
'deletes whole text',
'[foo]',
'[]'
);
test(
'deletes whole text between nodes',
'[foo]',
'[]'
);
test(
'deletes an element',
'x[]y',
'x[]y'
);
test(
'deletes a bunch of nodes',
'w[xy]z',
'w[]z'
);
test(
'does not break things when option.merge passed',
'w[xy]z',
'w[]z',
{ merge: true }
);
} );
describe( 'with text attributes', () => {
beforeEach( () => {
doc = new Document();
doc.createRoot();
const schema = doc.schema;
schema.registerItem( 'image', '$inline' );
schema.registerItem( 'paragraph', '$block' );
schema.allow( { name: '$text', inside: '$root' } );
schema.allow( { name: '$text', attributes: [ 'bold', 'italic' ] } );
} );
it( 'deletes characters (first half has attrs)', () => {
setData( doc, '<$text bold="true">fo[o$text>b]ar' );
deleteContents( doc.batch(), doc.selection );
expect( getData( doc ) ).to.equal( '<$text bold="true">fo[]$text>ar' );
expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
} );
it( 'deletes characters (2nd half has attrs)', () => {
setData( doc, 'fo[o<$text bold="true">b]ar$text>' );
deleteContents( doc.batch(), doc.selection );
expect( getData( doc ) ).to.equal( 'fo[]<$text bold="true">ar$text>' );
expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
} );
it( 'clears selection attrs when emptied content', () => {
setData( doc, 'x[<$text bold="true">foo$text>]y' );
deleteContents( doc.batch(), doc.selection );
expect( getData( doc ) ).to.equal( 'x[]y' );
expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
} );
it( 'leaves selection attributes when text contains them', () => {
setData(
doc,
'x<$text bold="true">a[foo]b$text>y',
{
selectionAttributes: {
bold: true
}
}
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc ) ).to.equal( 'x<$text bold="true">a[]b$text>y' );
expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
} );
} );
// Note: The algorithm does not care what kind of it's merging as it knows nothing useful about these elements.
// In most cases it handles all elements like you'd expect to handle block elements in HTML. However,
// in some scenarios where the tree depth is bigger results may be hard to justify. In fact, such cases
// should not happen unless we're talking about lists or tables, but these features will need to cover
// their scenarios themselves. In all generic scenarios elements are never nested.
//
// You may also be thinking – but I don't want my elements to be merged. It means that there are some special rules,
// like – multiple editing hosts (cE=true/false in use) or block limit elements like
.
// Those case should, again, be handled by their specific implementations.
describe( 'in multi-element scenarios', () => {
beforeEach( () => {
doc = new Document();
doc.createRoot();
const schema = doc.schema;
schema.registerItem( 'paragraph', '$block' );
schema.registerItem( 'heading1', '$block' );
schema.registerItem( 'pchild' );
schema.registerItem( 'image', '$inline' );
schema.allow( { name: 'pchild', inside: 'paragraph' } );
schema.allow( { name: 'paragraph', attributes: [ 'align' ] } );
} );
test(
'do not merge when no need to',
'x[foo]y',
'x[]y',
{ merge: true }
);
test(
'merges second element into the first one (same name)',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
test(
'does not merge second element into the first one (same name, !option.merge)',
'xfo[ob]ary',
'xfo[]ary'
);
test(
'merges second element into the first one (same name)',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
test(
'merges second element into the first one (different name)',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
it( 'merges second element into the first one (different name, backward selection)', () => {
setData(
doc,
'xfo[ob]ary',
{ lastRangeBackward: true }
);
deleteContents( doc.batch(), doc.selection, { merge: true } );
expect( getData( doc ) ).to.equal( 'xfo[]ary' );
} );
test(
'merges second element into the first one (different attrs)',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
test(
'merges second element to an empty first element',
'x[fo]oy',
'x[]oy',
{ merge: true }
);
test(
'merges elements when deep nested',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
// For code coverage reasons.
test(
'merges element when selection is in two consecutive nodes even when it is empty',
'foo[]bar',
'foo[]bar',
{ merge: true }
);
// If you disagree with this case please read the notes before this section.
test(
'merges elements when left end deep nested',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
// If you disagree with this case please read the notes before this section.
test(
'merges elements when right end deep nested',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
test(
'merges elements when more content in the right branch',
'xfo[ob]ary',
'xfo[]ary',
{ merge: true }
);
test(
'leaves just one element when all selected',
'[xfooy]',
'[]',
{ merge: true }
);
} );
describe( 'in element selections scenarios', () => {
beforeEach( () => {
doc = new Document();
// like root.
doc.createRoot( 'paragraph', 'paragraphRoot' );
// like root.
doc.createRoot( '$root', 'bodyRoot' );
// Special root which allows only blockWidgets inside itself.
doc.createRoot( 'restrictedRoot', 'restrictedRoot' );
const schema = doc.schema;
schema.registerItem( 'image', '$inline' );
schema.registerItem( 'paragraph', '$block' );
schema.registerItem( 'heading1', '$block' );
schema.registerItem( 'blockWidget' );
schema.registerItem( 'restrictedRoot' );
schema.allow( { name: '$block', inside: '$root' } );
schema.allow( { name: 'blockWidget', inside: '$root' } );
schema.allow( { name: 'blockWidget', inside: 'restrictedRoot' } );
} );
// See also "in simple scenarios => deletes an element".
it( 'deletes two inline elements', () => {
setData(
doc,
'x[]z',
{ rootName: 'paragraphRoot' }
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc, { rootName: 'paragraphRoot' } ) )
.to.equal( 'x[]z' );
} );
it( 'creates a paragraph when text is not allowed (paragraph selected)', () => {
setData(
doc,
'x[yyy]z',
{ rootName: 'bodyRoot' }
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc, { rootName: 'bodyRoot' } ) )
.to.equal( 'x[]z' );
} );
it( 'creates a paragraph when text is not allowed (block widget selected)', () => {
setData(
doc,
'x[]z',
{ rootName: 'bodyRoot' }
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc, { rootName: 'bodyRoot' } ) )
.to.equal( 'x[]z' );
} );
it( 'creates paragraph when text is not allowed (heading selected)', () => {
setData(
doc,
'x[yyy]z',
{ rootName: 'bodyRoot' }
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc, { rootName: 'bodyRoot' } ) )
.to.equal( 'x[]z' );
} );
it( 'creates paragraph when text is not allowed (two blocks selected)', () => {
setData(
doc,
'x[yyyyyy]z',
{ rootName: 'bodyRoot' }
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc, { rootName: 'bodyRoot' } ) )
.to.equal( 'x[]z' );
} );
it( 'creates paragraph when text is not allowed (all content selected)', () => {
setData(
doc,
'[xz]',
{ rootName: 'bodyRoot' }
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc, { rootName: 'bodyRoot' } ) )
.to.equal( '[]' );
} );
it( 'does not create a paragraph when it is not allowed', () => {
setData(
doc,
'[]',
{ rootName: 'restrictedRoot' }
);
deleteContents( doc.batch(), doc.selection );
expect( getData( doc, { rootName: 'restrictedRoot' } ) )
.to.equal( '[]' );
} );
} );
function test( title, input, output, options ) {
it( title, () => {
setData( doc, input );
deleteContents( doc.batch(), doc.selection, options );
expect( getData( doc ) ).to.equal( output );
} );
}
} );
} );
|