/** * @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[ob]ar' ); deleteContents( doc.batch(), doc.selection ); expect( getData( doc ) ).to.equal( '<$text bold="true">fo[]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' ); deleteContents( doc.batch(), doc.selection ); expect( getData( doc ) ).to.equal( 'fo[]<$text bold="true">ar' ); expect( doc.selection.getAttribute( 'bold' ) ).to.undefined; } ); it( 'clears selection attrs when emptied content', () => { setData( doc, 'x[<$text bold="true">foo]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]by', { selectionAttributes: { bold: true } } ); deleteContents( doc.batch(), doc.selection ); expect( getData( doc ) ).to.equal( 'x<$text bold="true">a[]by' ); 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 ); } ); } } ); } );