/** * @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 Model from '../../../src/model/model'; import insertContent from '../../../src/model/utils/insertcontent'; import DocumentFragment from '../../../src/model/documentfragment'; import Text from '../../../src/model/text'; import Element from '../../../src/model/element'; import Position from '../../../src/model/position'; import { setData, getData, parse, stringify } from '../../../src/dev-utils/model'; import Range from '../../../src/model/range'; describe( 'DataController utils', () => { let model, doc, root; describe( 'insertContent', () => { beforeEach( () => { model = new Model(); doc = model.document; root = doc.createRoot(); } ); it( 'should use parent batch', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'x[]x' ); model.change( writer => { insertContent( model, writer.createText( 'a' ) ); expect( writer.batch.operations ).to.length( 1 ); } ); } ); it( 'should be able to insert content at custom selection', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'a[]bc' ); const selection = model.createSelection( model.createPositionFromPath( doc.getRoot(), [ 2 ] ) ); model.change( writer => { const affectedRange = insertContent( model, writer.createText( 'x' ), selection ); expect( getData( model ) ).to.equal( 'a[]bxc' ); expect( stringify( root, affectedRange ) ).to.equal( 'ab[x]c' ); } ); } ); it( 'should modify passed selection instance', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'a[]bc' ); const selection = model.createSelection( model.createPositionFromPath( doc.getRoot(), [ 2 ] ) ); const selectionCopy = model.createSelection( model.createPositionFromPath( doc.getRoot(), [ 2 ] ) ); expect( selection.isEqual( selectionCopy ) ).to.be.true; model.change( writer => { insertContent( model, writer.createText( 'x' ), selection ); } ); expect( selection.isEqual( selectionCopy ) ).to.be.false; const insertionSelection = model.createSelection( model.createPositionFromPath( doc.getRoot(), [ 3 ] ) ); expect( selection.isEqual( insertionSelection ) ).to.be.true; } ); it( 'should be able to insert content at custom position', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'a[]bc' ); const position = new Position( doc.getRoot(), [ 2 ] ); model.change( writer => { const affectedRange = insertContent( model, writer.createText( 'x' ), position ); expect( getData( model ) ).to.equal( 'a[]bxc' ); expect( stringify( root, affectedRange ) ).to.equal( 'ab[x]c' ); } ); } ); it( 'should be able to insert content at custom range', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'a[]bc' ); const range = new Range( new Position( doc.getRoot(), [ 2 ] ), new Position( doc.getRoot(), [ 3 ] ) ); model.change( writer => { const affectedRange = insertContent( model, writer.createText( 'x' ), range ); expect( getData( model ) ).to.equal( 'a[]bx' ); expect( stringify( root, affectedRange ) ).to.equal( 'ab[x]' ); } ); } ); it( 'should be able to insert content at model selection if document selection is passed', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'a[]bc' ); model.change( writer => { const affectedRange = insertContent( model, writer.createText( 'x' ), model.document.selection ); expect( getData( model ) ).to.equal( 'ax[]bc' ); expect( stringify( root, affectedRange ) ).to.equal( 'a[x]bc' ); } ); } ); it( 'should be able to insert content at model selection if none passed', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'a[]bc' ); model.change( writer => { const affectedRange = insertContent( model, writer.createText( 'x' ) ); expect( getData( model ) ).to.equal( 'ax[]bc' ); expect( stringify( root, affectedRange ) ).to.equal( 'a[x]bc' ); } ); } ); it( 'should be able to insert content at model element (numeric offset)', () => { model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo[]bar' ); const element = doc.getRoot().getNodeByPath( [ 1 ] ); model.change( writer => { const text = writer.createText( 'x' ); const affectedRange = insertContent( model, text, element, 2 ); expect( getData( model ) ).to.equal( 'foo[]baxr' ); expect( stringify( root, affectedRange ) ).to.equal( 'fooba[x]r' ); } ); } ); it( 'should be able to insert content at model element (offset="in")', () => { model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo[]bar' ); const element = doc.getRoot().getNodeByPath( [ 1 ] ); model.change( writer => { const text = writer.createText( 'x' ); const affectedRange = insertContent( model, text, element, 'in' ); expect( getData( model ) ).to.equal( 'foo[]x' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[x]' ); } ); } ); it( 'should be able to insert content at model element (offset="on")', () => { model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); model.schema.register( 'foo', { inheritAllFrom: '$block' } ); setData( model, 'foo[]bar' ); const element = doc.getRoot().getNodeByPath( [ 1 ] ); model.change( writer => { const insertElement = writer.createElement( 'foo' ); const affectedRange = insertContent( model, insertElement, element, 'on' ); expect( getData( model ) ).to.equal( 'foo[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[]' ); } ); } ); it( 'should be able to insert content at model element (offset="end")', () => { model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); setData( model, 'foo[]bar' ); const element = doc.getRoot().getNodeByPath( [ 1 ] ); model.change( writer => { const text = writer.createText( 'x' ); const affectedRange = insertContent( model, text, element, 'end' ); expect( getData( model ) ).to.equal( 'foo[]barx' ); expect( stringify( root, affectedRange ) ).to.equal( 'foobar[x]' ); } ); } ); it( 'accepts DocumentFragment', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'x[]x' ); insertContent( model, new DocumentFragment( [ new Text( 'a' ) ] ) ); expect( getData( model ) ).to.equal( 'xa[]x' ); } ); it( 'accepts Text', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'x[]x' ); insertContent( model, new Text( 'a' ) ); expect( getData( model ) ).to.equal( 'xa[]x' ); } ); it( 'should save the reference to the original object', () => { const content = new Element( 'image' ); model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); model.schema.register( 'image', { allowWhere: '$text', isObject: true } ); setData( model, 'foo[]' ); insertContent( model, content ); expect( doc.getRoot().getChild( 0 ).getChild( 1 ) ).to.equal( content ); } ); describe( 'in simple scenarios', () => { beforeEach( () => { model = new Model(); doc = model.document; root = doc.createRoot(); const schema = model.schema; schema.register( 'image', { allowWhere: '$text', isObject: true } ); schema.register( 'disallowedElement' ); schema.extend( '$text', { allowIn: '$root' } ); schema.extend( 'image', { allowIn: '$root' } ); // Otherwise it won't be passed to the temporary model fragment used inside insert(). schema.extend( 'disallowedElement', { allowIn: '$clipboardHolder' } ); schema.extend( '$text', { allowIn: 'disallowedElement', allowAttributes: [ 'bold', 'italic' ] } ); } ); it( 'inserts one text node', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]oo' ); } ); it( 'inserts one text node (at the end)', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fooxyz[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xyz]' ); } ); it( 'inserts one text node with attribute', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( '<$text bold="true">xyz' ); expect( getData( model ) ).to.equal( 'f<$text bold="true">xyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[<$text bold="true">xyz]oo' ); expect( doc.selection.getAttribute( 'bold' ) ).to.be.true; } ); it( 'inserts one text node with attribute into text with a different attribute', () => { setData( model, '<$text bold="true">f[]oo' ); const affectedRange = insertHelper( '<$text italic="true">xyz' ); expect( getData( model ) ) .to.equal( '<$text bold="true">f<$text italic="true">xyz[]<$text bold="true">oo' ); expect( stringify( root, affectedRange ) ) .to.equal( '<$text bold="true">f[<$text italic="true">xyz]<$text bold="true">oo' ); expect( doc.selection.getAttribute( 'italic' ) ).to.be.true; expect( doc.selection.hasAttribute( 'bold' ) ).to.be.false; } ); it( 'inserts one text node with attribute into text with the same attribute', () => { setData( model, '<$text bold="true">f[]oo' ); const affectedRange = insertHelper( '<$text bold="true">xyz' ); expect( getData( model ) ).to.equal( '<$text bold="true">fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( '<$text bold="true">f[xyz]oo' ); expect( doc.selection.getAttribute( 'bold' ) ).to.be.true; } ); it( 'inserts a text without attributes into a text with an attribute', () => { setData( model, '<$text bold="true">f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( '<$text bold="true">fxyz[]<$text bold="true">oo' ); expect( stringify( root, affectedRange ) ).to.equal( '<$text bold="true">f[xyz]<$text bold="true">oo' ); expect( doc.selection.hasAttribute( 'bold' ) ).to.be.false; } ); it( 'inserts an element', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( 'f[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[]oo' ); } ); it( 'inserts a text and an element', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]oo' ); } ); it( 'strips a disallowed element', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]oo' ); } ); it( 'deletes selection before inserting the content', () => { setData( model, 'f[abc]oo' ); const affectedRange = insertHelper( 'x' ); expect( getData( model ) ).to.equal( 'fx[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[x]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( model, 'f[]oo' ); insertHelper( new Text( ' ' ) ); expect( getData( model ) ).to.equal( 'f []oo' ); } ); it( 'inserts three spaces', () => { setData( model, 'f[]oo' ); insertHelper( new Text( ' ' ) ); expect( getData( model ) ).to.equal( 'f []oo' ); } ); it( 'inserts spaces at the end', () => { setData( model, 'foo[]' ); insertHelper( new Text( ' ' ) ); expect( getData( model ) ).to.equal( 'foo []' ); } ); it( 'inserts one nbsp', () => { setData( model, 'f[]oo' ); insertHelper( new Text( '\u200a' ) ); expect( getData( model ) ).to.equal( 'f\u200a[]oo' ); } ); it( 'inserts word surrounded by spaces', () => { setData( model, 'f[]oo' ); insertHelper( new Text( ' xyz ' ) ); expect( getData( model ) ).to.equal( 'f xyz []oo' ); } ); } ); } ); describe( 'in blocks', () => { beforeEach( () => { model = new Model(); doc = model.document; root = doc.createRoot(); const schema = model.schema; schema.register( 'paragraph', { inheritAllFrom: '$block' } ); schema.register( 'heading1', { inheritAllFrom: '$block' } ); schema.register( 'heading2', { inheritAllFrom: '$block' } ); schema.register( 'blockWidget', { isObject: true, allowIn: '$root' } ); schema.register( 'inlineWidget', { isObject: true, allowIn: [ '$block', '$clipboardHolder' ] } ); schema.register( 'listItem', { inheritAllFrom: '$block', allowAttributes: [ 'listType', 'listIndent' ] } ); } ); it( 'inserts one text node', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]oo' ); } ); it( 'inserts one text node to fully selected paragraph', () => { setData( model, '[foo]' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'xyz[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[xyz]' ); } ); it( 'inserts one text node to fully selected paragraphs (from outside)', () => { setData( model, '[foobar]' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'xyz[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[xyz]' ); } ); it( 'merges two blocks before inserting content (p+p)', () => { setData( model, 'fo[ob]ar' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'foxyz[]ar' ); expect( stringify( root, affectedRange ) ).to.equal( 'fo[xyz]ar' ); } ); it( 'inserts inline widget and text', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]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( model, 'fo[ob]ar' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'foxyz[]ar' ); expect( stringify( root, affectedRange ) ).to.equal( 'fo[xyz]ar' ); } ); it( 'not insert autoparagraph when paragraph is disallowed at the current position', () => { // Disallow paragraph in $root. model.schema.addChildCheck( ( ctx, childDef ) => { if ( childDef.name == 'paragraph' && ctx.endsWith( '$root' ) ) { return false; } } ); const content = new DocumentFragment( [ new Element( 'heading1', [], [ new Text( 'bar' ) ] ), new Text( 'biz' ) ] ); setData( model, '[foo]' ); const affectedRange = insertContent( model, content ); expect( getData( model ) ).to.equal( 'bar[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[bar]' ); } ); describe( 'block to block handling', () => { it( 'inserts one paragraph', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]oo' ); } ); it( 'inserts one paragraph (at the end)', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fooxyz[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xyz]' ); } ); it( 'inserts one paragraph into an empty paragraph', () => { setData( model, '[]' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'xyz[]' ); // The empty paragraph gets removed and the new element is inserted instead. expect( stringify( root, affectedRange ) ).to.equal( '[xyz]' ); } ); it( 'inserts one empty paragraph', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( 'f[]oo' ); // Nothing is inserted so the `affectedRange` is collapsed at insertion position. expect( stringify( root, affectedRange ) ).to.equal( 'f[]oo' ); } ); it( 'inserts one block into a fully selected content', () => { setData( model, '[foobar]' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'xyz[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[xyz]' ); } ); it( 'inserts one heading', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]oo' ); } ); it( 'inserts two headings', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxxyyy' ); expect( getData( model ) ).to.equal( 'fxxxyyy[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxxyyy]oo' ); } ); it( 'inserts one object', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ) .to.equal( 'f[]oo' ); expect( stringify( root, affectedRange ) ) .to.equal( 'f[]oo' ); } ); it( 'inserts one object (at the end)', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( 'foo[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[]' ); } ); it( 'inserts one object (at the beginning)', () => { setData( model, '[]bar' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( '[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( '[]bar' ); } ); it( 'inserts one list item', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'fxyz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xyz]oo' ); } ); it( 'inserts list item to empty element', () => { setData( model, '[]' ); const affectedRange = insertHelper( 'xyz' ); expect( getData( model ) ).to.equal( 'xyz[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[xyz]' ); } ); it( 'inserts three list items at the end of paragraph', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'xxx' + 'yyy' + 'zzz' ); expect( getData( model ) ).to.equal( 'fooxxx' + 'yyy' + 'zzz[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxx' + 'yyy' + 'zzz]' ); } ); it( 'inserts two list items to an empty paragraph', () => { setData( model, 'a[]b' ); const affectedRange = insertHelper( 'xxx' + 'yyy' ); expect( getData( model ) ).to.equal( 'a' + 'xxx' + 'yyy[]' + 'b' ); expect( stringify( root, affectedRange ) ).to.equal( 'a' + '[xxx' + 'yyy]' + 'b' ); } ); it( 'should not merge a paragraph wrapped in blockQuote with list item (checking left merge)', () => { model.schema.register( 'blockQuote', { allowWhere: '$block', allowContentOf: '$root' } ); setData( model, 'fo[]o' ); const affectedRange = insertHelper( '
xxx
yyy' ); expect( getData( model ) ).to.equal( 'fo' + '
' + 'xxx' + '
' + 'yyy[]o' ); expect( stringify( root, affectedRange ) ).to.equal( 'fo[' + '
' + 'xxx' + '
' + 'yyy]o' ); } ); it( 'should not merge a paragraph wrapped in blockQuote with list item (checking right merge)', () => { model.schema.register( 'blockQuote', { allowWhere: '$block', allowContentOf: '$root' } ); setData( model, 'fo[]o' ); const affectedRange = insertHelper( 'yyy
xxx
' ); expect( getData( model ) ).to.equal( 'foyyy' + '
' + 'xxx' + '
' + '[]o' ); expect( stringify( root, affectedRange ) ).to.equal( 'fo[yyy' + '
' + 'xxx' + '
' + ']o' ); } ); it( 'should not merge a paragraph wrapped in blockQuote with list item (checking both merges)', () => { model.schema.register( 'blockQuote', { allowWhere: '$block', allowContentOf: '$root' } ); setData( model, 'fo[]o' ); const affectedRange = insertHelper( '
xxx
' ); expect( getData( model ) ).to.equal( 'fo' + '
' + 'xxx' + '
' + '[]o' ); expect( stringify( root, affectedRange ) ).to.equal( 'fo[' + '
' + 'xxx' + '
' + ']o' ); } ); } ); describe( 'mixed content to block', () => { it( 'inserts text + paragraph', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxxyyy' ); expect( getData( model ) ).to.equal( 'fxxxyyy[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxxyyy]oo' ); } ); it( 'inserts text + inlineWidget + text + paragraph', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxxyyyzzz' ); expect( getData( model ) ).to.equal( 'fxxxyyyzzz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxxyyyzzz]oo' ); } ); it( 'inserts text + paragraph (at the beginning)', () => { setData( model, '[]foo' ); const affectedRange = insertHelper( 'xxxyyy' ); expect( getData( model ) ).to.equal( 'xxxyyy[]foo' ); expect( stringify( root, affectedRange ) ).to.equal( '[xxxyyy]foo' ); } ); it( 'inserts text + paragraph (at the end)', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'xxxyyy' ); expect( getData( model ) ).to.equal( 'fooxxxyyy[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxxyyy]' ); } ); it( 'inserts paragraph + text', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'yyyxxx' ); expect( getData( model ) ).to.equal( 'fyyyxxx[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[yyyxxx]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( model, 'f[]oo' ); // insertHelper( 'yyyxxxzzz' ); // expect( getData( model ) ) // .to.equal( 'fyyyxxxzzz[]oo' ); // } ); // See the comment above. it( 'inserts paragraph + text + inlineWidget + text', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'yyyxxxzzz' ); expect( getData( model ) ).to.equal( 'fyyyxxx' + '' + 'zzz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[yyyxxx' + '' + 'zzz]oo' ); } ); it( 'inserts paragraph + text + paragraph', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'yyyxxxzzz' ); expect( getData( model ) ).to.equal( 'fyyyxxxzzz[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[yyyxxxzzz]oo' ); } ); it( 'inserts paragraph + text (at the beginning)', () => { setData( model, '[]foo' ); const affectedRange = insertHelper( 'yyyxxx' ); expect( getData( model ) ).to.equal( 'yyyxxx[]foo' ); expect( stringify( root, affectedRange ) ).to.equal( '[yyyxxx]foo' ); } ); it( 'inserts paragraph + text (at the end)', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'yyyxxx' ); expect( getData( model ) ).to.equal( 'fooyyyxxx[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[yyyxxx]' ); } ); it( 'inserts text + heading', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxxyyy' ); expect( getData( model ) ).to.equal( 'fxxxyyy[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxxyyy]oo' ); } ); it( 'inserts paragraph + object', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fxxx[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxx]oo' ); } ); it( 'inserts object + paragraph', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fxxx[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxx]oo' ); } ); it( 'inserts object + text', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fxxx[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxx]oo' ); } ); it( 'inserts object + text (at the beginning)', () => { setData( model, '[]foo' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'xxx[]foo' ); expect( stringify( root, affectedRange ) ).to.equal( '[xxx]foo' ); } ); it( 'inserts object + text (at the end)', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fooxxx[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxx]' ); } ); it( 'inserts text + object (at the end)', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fooxxx[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxx]' ); } ); } ); describe( 'content over a block object', () => { it( 'inserts text', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fooxxx[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxx]bar' ); } ); it( 'inserts paragraph', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ) .to.equal( 'fooxxx[]bar' ); expect( stringify( root, affectedRange ) ) .to.equal( 'foo[xxx]bar' ); } ); it( 'inserts text + paragraph', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'yyyxxx' ); expect( getData( model ) ).to.equal( 'fooyyyxxx[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[yyyxxx]bar' ); } ); it( 'inserts two blocks', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'xxxyyy' ); expect( getData( model ) ).to.equal( 'fooxxxyyy[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxxyyy]bar' ); } ); it( 'inserts block object', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( '' ); // It's enough, don't worry. expect( getData( model ) ).to.equal( 'foo[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[]bar' ); } ); it( 'inserts inline object', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( 'foo[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[]bar' ); } ); } ); describe( 'content over an inline object', () => { it( 'inserts text', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fooxxx[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxx]bar' ); } ); it( 'inserts paragraph', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'fooxxx[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxx]bar' ); } ); it( 'inserts text + paragraph', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'yyyxxx' ); expect( getData( model ) ).to.equal( 'fooyyyxxx[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[yyyxxx]bar' ); } ); it( 'inserts two blocks', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( 'xxxyyy' ); expect( getData( model ) ).to.equal( 'fooxxxyyy[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[xxxyyy]bar' ); } ); it( 'inserts inline object', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( 'foo[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[]bar' ); } ); it( 'inserts block object', () => { setData( model, 'foo[]bar' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( 'foo[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[]bar' ); } ); } ); } ); describe( 'filtering out', () => { beforeEach( () => { model = new Model(); doc = model.document; root = doc.createRoot(); const schema = model.schema; schema.register( 'paragraph', { inheritAllFrom: '$block' } ); schema.register( 'heading1', { inheritAllFrom: '$block' } ); schema.register( 'element', { inheritAllFrom: '$block' } ); schema.register( 'table' ); schema.register( 'td' ); schema.register( 'disallowedWidget', { isObject: true } ); schema.extend( 'table', { allowIn: '$clipboardHolder' } ); schema.extend( 'td', { allowIn: '$clipboardHolder' } ); schema.extend( 'td', { allowIn: 'table' } ); schema.extend( 'element', { allowIn: 'td' } ); schema.extend( '$block', { allowIn: 'td' } ); schema.extend( '$text', { allowIn: 'td' } ); schema.extend( 'table', { allowIn: 'element' } ); schema.extend( 'disallowedWidget', { allowIn: '$clipboardHolder' } ); schema.extend( '$text', { allowIn: 'disallowedWidget' } ); schema.extend( 'element', { allowIn: 'paragraph' } ); schema.extend( 'element', { allowIn: 'heading1' } ); schema.addAttributeCheck( ( ctx, attributeName ) => { // Allow 'b' on paragraph>$text. if ( ctx.endsWith( 'paragraph $text' ) && attributeName == 'b' ) { return true; } // Allow 'b' on paragraph>element>$text. if ( ctx.endsWith( 'paragraph element $text' ) && attributeName == 'b' ) { return true; } // Allow 'a' and 'b' on heading1>element>$text. if ( ctx.endsWith( 'heading1 element $text' ) && [ 'a', 'b' ].includes( attributeName ) ) { return true; } // Allow 'b' on element>table>td>$text. if ( ctx.endsWith( 'element table td $text' ) && attributeName == 'b' ) { return true; } } ); } ); it( 'filters out disallowed elements and leaves out the text', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( '
xxxyyy
' ); expect( getData( model ) ).to.equal( 'fxxxyyy[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[xxxyyy]oo' ); } ); it( 'filters out disallowed elements and leaves out the paragraphs', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( '
xxxyyyzzz
' ); expect( getData( model ) ) .to.equal( 'fxxxyyyzzz[]oo' ); expect( stringify( root, affectedRange ) ) .to.equal( 'f[xxxyyyzzz]oo' ); } ); it( 'filters out disallowed objects', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxx' ); expect( getData( model ) ).to.equal( 'f[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[]oo' ); } ); it( 'filters out disallowed attributes when inserting text', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'x<$text a="1" b="1">xxy<$text a="1">yy' ); expect( getData( model ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[x<$text b="1">xxyyy]oo' ); } ); it( 'filters out disallowed attributes when inserting nested elements', () => { setData( model, '[]' ); const affectedRange = insertHelper( '
f<$text a="1" b="1" c="1">oo
' ); expect( getData( model ) ) .to.equal( '
f<$text b="1">oo
[]
' ); expect( stringify( root, affectedRange ) ) .to.equal( '[
f<$text b="1">oo
]
' ); } ); it( 'filters out disallowed attributes when inserting text in disallowed elements', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( '
x<$text a="1" b="1">xxy<$text a="1">yy
' ); expect( getData( model ) ).to.equal( 'fx<$text b="1">xxyyy[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[x<$text b="1">xxyyy]oo' ); } ); it( 'filters out disallowed attributes when merging #1', () => { setData( model, '[]foo' ); const affectedRange = insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( model ) ).to.equal( 'x<$text b="1">xx[]foo' ); expect( stringify( root, affectedRange ) ).to.equal( '[x<$text b="1">xx]foo' ); } ); it( 'filters out disallowed attributes when merging #2', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( model ) ).to.equal( 'fx<$text b="1">xx[]oo' ); expect( stringify( root, affectedRange ) ).to.equal( 'f[x<$text b="1">xx]oo' ); } ); it( 'filters out disallowed attributes when merging #3', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'x<$text a="1" b="1">xx' ); expect( getData( model ) ).to.equal( 'foox<$text b="1">xx[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[x<$text b="1">xx]' ); } ); it( 'filters out disallowed attributes from nested nodes when merging', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xb<$text a="1" b="1">arx' ); expect( getData( model ) ) .to.equal( 'fxb<$text b="1">arx[]oo' ); expect( stringify( root, affectedRange ) ) .to.equal( 'f[xb<$text b="1">arx]oo' ); } ); it( 'filters out disallowed attributes when autoparagraphing', () => { setData( model, 'f[]oo' ); const affectedRange = insertHelper( 'xxx<$text a="1" b="1">yyy' ); expect( getData( model ) ) .to.equal( 'fxxx<$text b="1">yyy[]oo' ); expect( stringify( root, affectedRange ) ) .to.equal( 'f[xxx<$text b="1">yyy]oo' ); } ); } ); } ); describe( 'integration with limit elements', () => { beforeEach( () => { model = new Model(); doc = model.document; root = doc.createRoot(); const schema = model.schema; schema.register( 'limit', { isLimit: true } ); schema.extend( 'limit', { allowIn: '$root' } ); schema.extend( '$text', { allowIn: 'limit' } ); schema.register( 'disallowedElement' ); schema.extend( 'disallowedElement', { allowIn: '$clipboardHolder' } ); schema.register( 'paragraph', { inheritAllFrom: '$block' } ); } ); it( 'should insert limit element', () => { const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( '[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[]' ); } ); it( 'should insert text into limit element', () => { setData( model, '[]' ); const affectedRange = insertHelper( 'foo bar' ); expect( getData( model ) ).to.equal( 'foo bar[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[foo bar]' ); } ); it( 'should insert text into limit element when selection spans over many limit elements', () => { let affectedRange; model.enqueueChange( 'transparent', () => { setData( model, 'foo[]bar' ); affectedRange = insertHelper( 'baz' ); } ); expect( getData( model ) ).to.equal( 'foobaz[]bar' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[baz]bar' ); } ); it( 'should not insert disallowed elements inside limit elements', () => { setData( model, '[]' ); const affectedRange = insertHelper( '' ); expect( getData( model ) ).to.equal( '[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[]' ); } ); it( 'should not leave the limit element when inserting at the end', () => { setData( model, 'foo[]' ); const affectedRange = insertHelper( 'ab' ); expect( getData( model ) ).to.equal( 'fooab[]' ); expect( stringify( root, affectedRange ) ).to.equal( 'foo[ab]' ); } ); it( 'should not leave the limit element when inserting at the beginning', () => { setData( model, '[]foo' ); const affectedRange = insertHelper( 'ab' ); expect( getData( model ) ).to.equal( 'ab[]foo' ); expect( stringify( root, affectedRange ) ).to.equal( '[ab]foo' ); } ); describe( 'when allowed element is above limit element in document tree', () => { // $root > table > tableRow > tableCell > paragraph // After inserting new table ( allowed in root ), empty paragraph shouldn't be removed from current tableCell. beforeEach( () => { const schema = model.schema; schema.register( 'wrapper', { isLimit: true, isBlock: true, isObject: true, allowWhere: '$block' } ); schema.extend( 'paragraph', { allowIn: 'limit' } ); schema.extend( 'limit', { allowIn: 'wrapper' } ); } ); it( 'should not remove empty elements when not-allowed element is paste', () => { setData( model, '[]' ); // Pasted content is forbidden in current selection. const affectedRange = insertHelper( 'foo' ); expect( getData( model ) ).to.equal( '[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[]' ); } ); it( 'should correctly paste allowed nodes', () => { setData( model, '[]' ); const affectedRange = insertHelper( 'foo' ); expect( getData( model ) ).to.equal( 'foo[]' ); expect( stringify( root, affectedRange ) ).to.equal( '[foo]' ); } ); } ); } ); // Helper function that parses given content and inserts it at the cursor position. // // @param {module:engine/model/item~Item|String} content // @returns {module:engine/model/range~Range} range function insertHelper( content ) { if ( typeof content == 'string' ) { content = parse( content, model.schema, { context: [ '$clipboardHolder' ] } ); } return insertContent( model, content ); } } );