/** * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ /* bender-tags: model */ import Document from 'ckeditor5/engine/model/document.js'; import DataController from 'ckeditor5/engine/controller/datacontroller.js'; import insert from 'ckeditor5/engine/controller/insert.js'; import ViewDocumentFragment from 'ckeditor5/engine/view/documentfragment.js'; import ViewText from 'ckeditor5/engine/view/text.js'; import ModelDocumentFragment from 'ckeditor5/engine/model/documentfragment.js'; import Text from 'ckeditor5/engine/model/text.js'; import { setData, getData, parse } from 'ckeditor5/engine/dev-utils/model.js'; describe( 'DataController', () => { let doc, dataController; describe( 'insert', () => { it( 'uses the passed batch', () => { doc = new Document(); doc.createRoot(); doc.schema.allow( { name: '$text', inside: '$root' } ); dataController = new DataController( doc ); const batch = doc.batch(); setData( doc, 'x[]x' ); insert( dataController, new ViewDocumentFragment( [ new ViewText( 'a' ) ] ), doc.selection, batch ); expect( batch.deltas.length ).to.be.above( 0 ); } ); describe( 'in simple scenarios', () => { beforeEach( () => { doc = new Document(); doc.createRoot(); dataController = new DataController( doc ); const schema = doc.schema; schema.registerItem( 'image', '$inline' ); schema.registerItem( 'disallowedElement' ); schema.allow( { name: '$text', inside: '$root' } ); schema.allow( { name: 'image', inside: '$root' } ); // Otherwise it won't be passed to the temporary model fragment used inside insert(). schema.allow( { name: 'disallowedElement', inside: '$clipboardHolder' } ); schema.objects.add( 'image' ); } ); it( 'inserts one text node', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); it( 'inserts one text node (at the end)', () => { setData( doc, 'foo[]' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fooxyz[]' ); } ); it( 'inserts an element', () => { setData( doc, 'f[]oo' ); insertHelper( '' ); expect( getData( doc ) ).to.equal( 'f[]oo' ); } ); it( 'inserts a text and an element', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); it( 'strips a disallowed element', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); it( 'deletes selection before inserting the content', () => { setData( doc, 'f[abc]oo' ); insertHelper( 'x' ); expect( getData( doc ) ).to.equal( 'fx[]oo' ); } ); describe( 'spaces handling', () => { // Note: spaces in the view are not encoded like in the DOM, so subsequent spaces must be // inserted into the model as is. The conversion to nbsps happen on view<=>DOM conversion. it( 'inserts one space', () => { setData( doc, 'f[]oo' ); insertHelper( new Text( ' ' ) ); expect( getData( doc ) ).to.equal( 'f []oo' ); } ); it( 'inserts three spaces', () => { setData( doc, 'f[]oo' ); insertHelper( new Text( ' ' ) ); expect( getData( doc ) ).to.equal( 'f []oo' ); } ); it( 'inserts spaces at the end', () => { setData( doc, 'foo[]' ); insertHelper( new Text( ' ' ) ); expect( getData( doc ) ).to.equal( 'foo []' ); } ); it( 'inserts one nbsp', () => { setData( doc, 'f[]oo' ); insertHelper( new Text( '\u200a' ) ); expect( getData( doc ) ).to.equal( 'f\u200a[]oo' ); } ); it( 'inserts word surrounded by spaces', () => { setData( doc, 'f[]oo' ); insertHelper( new Text( ' xyz ' ) ); expect( getData( doc ) ).to.equal( 'f xyz []oo' ); } ); } ); } ); describe( 'in blocks', () => { beforeEach( () => { doc = new Document(); doc.createRoot(); dataController = new DataController( doc ); const schema = doc.schema; schema.registerItem( 'paragraph', '$block' ); schema.registerItem( 'heading1', '$block' ); schema.registerItem( 'heading2', '$block' ); schema.registerItem( 'blockWidget' ); schema.registerItem( 'inlineWidget' ); schema.registerItem( 'listItem', '$block' ); schema.allow( { name: 'blockWidget', inside: '$root' } ); schema.allow( { name: 'inlineWidget', inside: '$block' } ); schema.allow( { name: 'inlineWidget', inside: '$clipboardHolder' } ); schema.allow( { name: 'listItem', inside: '$root', attributes: [ 'type', 'indent' ] } ); schema.requireAttributes( 'listItem', [ 'type', 'indent' ] ); schema.objects.add( 'blockWidget' ); schema.objects.add( 'inlineWidget' ); } ); it( 'inserts one text node', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); it( 'inserts one text node to fully selected paragraph', () => { setData( doc, '[foo]' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'xyz[]' ); } ); it( 'inserts one text node to fully selected paragraphs (from outside)', () => { setData( doc, '[foobar]' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'xyz[]' ); } ); it( 'merges two blocks before inserting content (p+p)', () => { setData( doc, 'fo[ob]ar' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'foxyz[]ar' ); } ); it( 'inserts inline widget and text', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); // Note: In CKEditor 4 the blocks are not merged, but to KISS we're merging here // because that's what deleteContent() does. it( 'merges two blocks before inserting content (h+p)', () => { setData( doc, 'fo[ob]ar' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'foxyz[]ar' ); } ); describe( 'block to block handling', () => { it( 'inserts one paragraph', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); it( 'inserts one paragraph (at the end)', () => { setData( doc, 'foo[]' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fooxyz[]' ); } ); it( 'inserts one paragraph into an empty paragraph', () => { setData( doc, '[]' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'xyz[]' ); } ); it( 'inserts one block into a fully selected content', () => { setData( doc, '[foobar]' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'xyz[]' ); } ); it( 'inserts one heading', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); it( 'inserts two headings', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxxyyy' ); expect( getData( doc ) ).to.equal( 'fxxxyyy[]oo' ); } ); it( 'inserts one object', () => { setData( doc, 'f[]oo' ); insertHelper( '' ); expect( getData( doc ) ).to.equal( 'f[]oo' ); } ); it( 'inserts one object (at the end)', () => { setData( doc, 'foo[]' ); insertHelper( '' ); expect( getData( doc ) ).to.equal( 'foo[]' ); } ); it( 'inserts one object (at the beginning)', () => { setData( doc, '[]bar' ); insertHelper( '' ); expect( getData( doc ) ).to.equal( '[]bar' ); } ); it( 'inserts one list item', () => { setData( doc, 'f[]oo' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'fxyz[]oo' ); } ); it( 'inserts list item to empty element', () => { setData( doc, '[]' ); insertHelper( 'xyz' ); expect( getData( doc ) ).to.equal( 'xyz[]' ); } ); it( 'inserts three list items at the end of paragraph', () => { setData( doc, 'foo[]' ); insertHelper( 'xxx' + 'yyy' + 'zzz' ); expect( getData( doc ) ).to.equal( 'fooxxx' + 'yyy' + 'zzz[]' ); } ); it( 'inserts two list items to an empty paragraph', () => { setData( doc, 'a[]b' ); insertHelper( 'xxx' + 'yyy' ); expect( getData( doc ) ).to.equal( 'a' + 'xxx' + 'yyy[]' + 'b' ); } ); } ); describe( 'mixed content to block', () => { it( 'inserts text + paragraph', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxxyyy' ); expect( getData( doc ) ).to.equal( 'fxxxyyy[]oo' ); } ); it( 'inserts text + inlineWidget + text + paragraph', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxxyyyzzz' ); expect( getData( doc ) ).to.equal( 'fxxxyyyzzz[]oo' ); } ); it( 'inserts text + paragraph (at the beginning)', () => { setData( doc, '[]foo' ); insertHelper( 'xxxyyy' ); expect( getData( doc ) ).to.equal( 'xxxyyy[]foo' ); } ); it( 'inserts text + paragraph (at the end)', () => { setData( doc, 'foo[]' ); insertHelper( 'xxxyyy' ); expect( getData( doc ) ).to.equal( 'fooxxxyyy[]' ); } ); it( 'inserts paragraph + text', () => { setData( doc, 'f[]oo' ); insertHelper( 'yyyxxx' ); expect( getData( doc ) ).to.equal( 'fyyyxxx[]oo' ); } ); // This is the expected result, but it was so hard to achieve at this stage that I // decided to go with the what the next test represents. // it( 'inserts paragraph + text + inlineWidget + text', () => { // setData( doc, 'f[]oo' ); // insertHelper( 'yyyxxxzzz' ); // expect( getData( doc ) ) // .to.equal( 'fyyyxxxzzz[]oo' ); // } ); // See the comment above. it( 'inserts paragraph + text + inlineWidget + text', () => { setData( doc, 'f[]oo' ); insertHelper( 'yyyxxxzzz' ); expect( getData( doc ) ).to.equal( 'fyyyxxx' + '' + 'zzz[]oo' ); } ); it( 'inserts paragraph + text + paragraph', () => { setData( doc, 'f[]oo' ); insertHelper( 'yyyxxxzzz' ); expect( getData( doc ) ).to.equal( 'fyyyxxxzzz[]oo' ); } ); it( 'inserts paragraph + text (at the beginning)', () => { setData( doc, '[]foo' ); insertHelper( 'yyyxxx' ); expect( getData( doc ) ).to.equal( 'yyyxxx[]foo' ); } ); it( 'inserts paragraph + text (at the end)', () => { setData( doc, 'foo[]' ); insertHelper( 'yyyxxx' ); expect( getData( doc ) ).to.equal( 'fooyyyxxx[]' ); } ); it( 'inserts text + heading', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxxyyy' ); expect( getData( doc ) ).to.equal( 'fxxxyyy[]oo' ); } ); it( 'inserts paragraph + object', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'fxxx[]oo' ); } ); it( 'inserts object + paragraph', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'fxxx[]oo' ); } ); } ); describe( 'content over a block object', () => { it( 'inserts text', () => { setData( doc, 'foo[]bar' ); insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'fooxxx[]bar' ); } ); it( 'inserts paragraph', () => { setData( doc, 'foo[]bar' ); insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'fooxxx[]bar' ); } ); it( 'inserts text + paragraph', () => { setData( doc, 'foo[]bar' ); insertHelper( 'yyyxxx' ); expect( getData( doc ) ) .to.equal( 'fooyyyxxx[]bar' ); } ); it( 'inserts two blocks', () => { setData( doc, 'foo[]bar' ); insertHelper( 'xxxyyy' ); expect( getData( doc ) ) .to.equal( 'fooxxxyyy[]bar' ); } ); it( 'inserts block object', () => { setData( doc, 'foo[]bar' ); insertHelper( '' ); // It's enough, don't worry. expect( getData( doc ) ).to.equal( 'foo[]bar' ); } ); it( 'inserts inline object', () => { setData( doc, 'foo[]bar' ); insertHelper( '' ); expect( getData( doc ) ) .to.equal( 'foo[]bar' ); } ); } ); describe( 'content over an inline object', () => { it( 'inserts text', () => { setData( doc, 'foo[]bar' ); insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'fooxxx[]bar' ); } ); it( 'inserts paragraph', () => { setData( doc, 'foo[]bar' ); insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'fooxxx[]bar' ); } ); it( 'inserts text + paragraph', () => { setData( doc, 'foo[]bar' ); insertHelper( 'yyyxxx' ); expect( getData( doc ) ).to.equal( 'fooyyyxxx[]bar' ); } ); it( 'inserts two blocks', () => { setData( doc, 'foo[]bar' ); insertHelper( 'xxxyyy' ); expect( getData( doc ) ).to.equal( 'fooxxxyyy[]bar' ); } ); it( 'inserts inline object', () => { setData( doc, 'foo[]bar' ); insertHelper( '' ); expect( getData( doc ) ).to.equal( 'foo[]bar' ); } ); it( 'inserts block object', () => { setData( doc, 'foo[]bar' ); insertHelper( '' ); expect( getData( doc ) ).to.equal( 'foo[]bar' ); } ); } ); } ); describe( 'filtering out', () => { beforeEach( () => { doc = new Document(); doc.createRoot(); dataController = new DataController( doc ); const schema = doc.schema; schema.registerItem( 'paragraph', '$block' ); // Let's use table as an example of content which needs to be filtered out. schema.registerItem( 'table' ); schema.registerItem( 'td' ); schema.registerItem( 'disallowedWidget' ); schema.allow( { name: 'table', inside: '$clipboardHolder' } ); schema.allow( { name: 'td', inside: '$clipboardHolder' } ); schema.allow( { name: '$block', inside: 'td' } ); schema.allow( { name: '$text', inside: 'td' } ); schema.allow( { name: 'disallowedWidget', inside: '$clipboardHolder' } ); schema.allow( { name: '$text', inside: 'disallowedWidget' } ); schema.objects.add( 'disallowedWidget' ); } ); it( 'filters out disallowed elements and leaves out the text', () => { setData( doc, 'f[]oo' ); insertHelper( '
xxxyyy
' ); expect( getData( doc ) ).to.equal( 'fxxxyyy[]oo' ); } ); it( 'filters out disallowed elements and leaves out the paragraphs', () => { setData( doc, 'f[]oo' ); insertHelper( '
xxxyyyzzz
' ); expect( getData( doc ) ).to.equal( 'fxxxyyyzzz[]oo' ); } ); it( 'filters out disallowed objects', () => { setData( doc, 'f[]oo' ); insertHelper( 'xxx' ); expect( getData( doc ) ).to.equal( 'f[]oo' ); } ); } ); describe( 'special schema configurations', () => { beforeEach( () => { doc = new Document(); doc.createRoot(); dataController = new DataController( doc ); } ); it( 'should not break when autoparagraphing of text is not possible', () => { const schema = doc.schema; schema.registerItem( 'noTextAllowed' ); schema.registerItem( 'object' ); schema.allow( { name: 'noTextAllowed', inside: '$root' } ); schema.allow( { name: 'object', inside: 'noTextAllowed' } ); schema.objects.add( 'object' ); setData( doc, '[]' ); insertHelper( 'foo' ); expect( getData( doc ) ).to.equal( '[]' ); } ); } ); } ); // @param {engine.model.Item|String} content function insertHelper( content ) { if ( typeof content == 'string' ) { content = parse( content, doc.schema, { context: [ '$clipboardHolder' ] } ); } if ( !( content instanceof ModelDocumentFragment ) ) { content = new ModelDocumentFragment( [ content ] ); } // Override the convertion so we get exactly the model that we defined in the content param. // This way we avoid the need to write converters for everything we want to test. dataController.viewToModel.convert = () => { return content; }; insert( dataController, new ViewDocumentFragment(), doc.selection ); } } );