/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import Model from '../../../src/model/model'; import Selection from '../../../src/model/selection'; import modifySelection from '../../../src/model/utils/modifyselection'; import { setData, stringify } from '../../../src/dev-utils/model'; describe( 'DataController utils', () => { let model, doc; beforeEach( () => { model = new Model(); doc = model.document; model.schema.register( 'p', { inheritAllFrom: '$block' } ); model.schema.register( 'x', { inheritAllFrom: '$block' } ); model.schema.extend( 'x', { allowIn: 'p' } ); doc.createRoot(); } ); describe( 'modifySelection', () => { describe( 'unit=character', () => { describe( 'within element', () => { test( 'does nothing on empty content', '[]', '[]' ); test( 'does nothing on empty content (with empty element)', '

[]

', '

[]

' ); test( 'does nothing on empty content (backward)', '[]', '[]', { direction: 'backward' } ); test( 'does nothing on root boundary', '

foo[]

', '

foo[]

' ); test( 'does nothing on root boundary (backward)', '

[]foo

', '

[]foo

', { direction: 'backward' } ); test( 'extends one character forward', '

f[]oo

', '

f[o]o

' ); it( 'extends one character backward', () => { setData( model, '

fo[]o

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

f[o]o

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends one character forward (non-collapsed)', '

f[o]obar

', '

f[oo]bar

' ); it( 'extends one character backward (non-collapsed)', () => { setData( model, '

foob[a]r

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

foo[ba]r

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends to element boundary', '

fo[]o

', '

fo[o]

' ); it( 'extends to element boundary (backward)', () => { setData( model, '

f[]oo

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[f]oo

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'shrinks forward selection (to collapsed)', '

foo[b]ar

', '

foo[]bar

', { direction: 'backward' } ); it( 'shrinks backward selection (to collapsed)', () => { setData( model, '

foo[b]ar

', { lastRangeBackward: true } ); modifySelection( model, doc.selection ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

foob[]ar

' ); expect( doc.selection.isBackward ).to.false; } ); test( 'unicode support - combining mark forward', '

foo[]b̂ar

', '

foo[b̂]ar

' ); it( 'unicode support - combining mark backward', () => { setData( model, '

foob̂[]ar

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

foo[b̂]ar

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark multiple', '

fo[]o̻̐ͩbar

', '

fo[o̻̐ͩ]bar

' ); it( 'unicode support - combining mark multiple backward', () => { setData( model, '

foo̻̐ͩ[]bar

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

fo[o̻̐ͩ]bar

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark to the end', '

fo[]o̻̐ͩ

', '

fo[o̻̐ͩ]

' ); test( 'unicode support - surrogate pairs forward', '

[]\uD83D\uDCA9

', '

[\uD83D\uDCA9]

' ); it( 'unicode support - surrogate pairs backward', () => { setData( model, '

\uD83D\uDCA9[]

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[\uD83D\uDCA9]

' ); expect( doc.selection.isBackward ).to.true; } ); } ); describe( 'beyond element', () => { test( 'extends over boundary of empty elements', '

[]

', '

[

]

' ); it( 'extends over boundary of empty elements (backward)', () => { setData( model, '

[]

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[

]

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends over boundary of non-empty elements', '

a[]

bcd

', '

a[

]bcd

' ); it( 'extends over boundary of non-empty elements (backward)', () => { setData( model, '

a

[]bcd

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

a[

]bcd

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends over character after boundary', '

a[

]bcd

', '

a[

b]cd

' ); it( 'extends over character after boundary (backward)', () => { setData( model, '

abc[

]d

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

ab[c

]d

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'stops on the first position where text is allowed - inside block', '

a[]

bcd

', '

a[

]bcd

' ); test( 'stops on the first position where text is allowed - inside inline element', '

a[

]bcdef

', '

a[

]bcdef

' ); test( 'extends over element when next node is a text', '

a[]bc

', '

a[]bc

' ); test( 'extends over element when next node is a text - backward', '

ab[]c

', '

ab[]c

', { direction: 'backward' } ); it( 'shrinks over boundary of empty elements', () => { setData( model, '

[

]

', { lastRangeBackward: true } ); modifySelection( model, doc.selection ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[]

' ); expect( doc.selection.isBackward ).to.false; } ); it( 'shrinks over boundary of empty elements (backward)', () => { setData( model, '

[

]

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[]

' ); expect( doc.selection.isBackward ).to.false; } ); it( 'shrinks over boundary of non-empty elements', () => { setData( model, '

a[

]b

', { lastRangeBackward: true } ); modifySelection( model, doc.selection ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

a

[]b

' ); expect( doc.selection.isBackward ).to.false; } ); test( 'shrinks over boundary of non-empty elements (backward)', '

a[

]b

', '

a[]

b

', { direction: 'backward' } ); it( 'updates selection attributes', () => { setData( model, '

<$text bold="true">foo[b]

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

<$text bold="true">foo[]b

' ); expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true ); } ); } ); describe( 'beyond element – skipping incorrect positions', () => { beforeEach( () => { model.schema.register( 'quote' ); model.schema.extend( 'quote', { allowIn: '$root' } ); model.schema.extend( '$block', { allowIn: 'quote' } ); } ); test( 'skips position at the beginning of an element which does not allow text', '

x[]

y

z

', '

x[

]y

z

' ); test( 'skips position at the end of an element which does not allow text - backward', '

x

y

[]z

', '

x

y[

]z

', { direction: 'backward' } ); test( 'skips position at the end of an element which does not allow text', '

x[

y]

z

', '

x[

y

]z

' ); test( 'skips position at the beginning of an element which does not allow text - backward', '

x

[]y

z

', '

x[

]y

z

', { direction: 'backward' } ); test( 'extends to an empty block after skipping incorrect position', '

x[]

z

', '

x[

]

z

' ); test( 'extends to an empty block after skipping incorrect position - backward', '

x

[]z

', '

x

[

]z

', { direction: 'backward' } ); } ); } ); describe( 'unit=codePoint', () => { it( 'does nothing on empty content', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, '' ); modifySelection( model, doc.selection, { unit: 'codePoint' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[]' ); } ); test( 'does nothing on empty content (with empty element)', '

[]

', '

[]

', { unit: 'codePoint' } ); test( 'extends one user-perceived character forward - latin letters', '

f[]oo

', '

f[o]o

', { unit: 'codePoint' } ); it( 'extends one user-perceived character backward - latin letters', () => { setData( model, '

fo[]o

' ); modifySelection( model, doc.selection, { unit: 'codePoint', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

f[o]o

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark forward', '

foo[]b̂ar

', '

foo[b]̂ar

', { unit: 'codePoint' } ); it( 'unicode support - combining mark backward', () => { setData( model, '

foob̂[]ar

' ); // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection. // Document's selection will throw errors in some test cases (which are correct cases, but only for // non-document selections). const testSelection = new Selection( doc.selection ); modifySelection( model, testSelection, { unit: 'codePoint', direction: 'backward' } ); expect( stringify( doc.getRoot(), testSelection ) ).to.equal( '

foob[̂]ar

' ); expect( testSelection.isBackward ).to.true; } ); test( 'unicode support - combining mark multiple', '

fo[]o̻̐ͩbar

', '

fo[o]̻̐ͩbar

', { unit: 'codePoint' } ); test( 'unicode support - surrogate pairs forward', '

[]\uD83D\uDCA9

', '

[\uD83D\uDCA9]

', { unit: 'codePoint' } ); it( 'unicode support surrogate pairs backward', () => { setData( model, '

\uD83D\uDCA9[]

' ); modifySelection( model, doc.selection, { unit: 'codePoint', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[\uD83D\uDCA9]

' ); expect( doc.selection.isBackward ).to.true; } ); } ); describe( 'unit=word', () => { describe( 'within element', () => { test( 'does nothing on empty content', '[]', '[]', { unit: 'word' } ); test( 'does nothing on empty content (with empty element)', '

[]

', '

[]

' ); test( 'does nothing on empty content (backward)', '[]', '[]', { unit: 'word', direction: 'backward' } ); test( 'does nothing on root boundary', '

foo[]

', '

foo[]

', { unit: 'word' } ); test( 'does nothing on root boundary (backward)', '

[]foo

', '

[]foo

', { unit: 'word', direction: 'backward' } ); for ( const char of ' ,.?!:;"-()'.split( '' ) ) { testStopCharacter( char ); } test( 'extends whole word forward (non-collapsed)', '

f[o]obar

', '

f[oobar]

', { unit: 'word' } ); it( 'extends whole word backward (non-collapsed)', () => { setData( model, '

foo ba[a]r

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

foo [baa]r

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends to element boundary', '

fo[]oo

', '

fo[oo]

', { unit: 'word' } ); it( 'extends to element boundary (backward)', () => { setData( model, '

ff[]oo

' ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[ff]oo

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'expands forward selection to the word start', '

foo bar[b]az

', '

foo [bar]baz

', { unit: 'word', direction: 'backward' } ); it( 'expands backward selection to the word end', () => { setData( model, '

foo[b]ar baz

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { unit: 'word' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

foob[ar] baz

' ); expect( doc.selection.isBackward ).to.false; } ); test( 'unicode support - combining mark forward', '

foo[]b̂ar

', '

foo[b̂ar]

', { unit: 'word' } ); it( 'unicode support - combining mark backward', () => { setData( model, '

foob̂[]ar

' ); modifySelection( model, doc.selection, { direction: 'backward', unit: 'word' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[foob̂]ar

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark multiple', '

fo[]o̻̐ͩbar

', '

fo[o̻̐ͩbar]

', { unit: 'word' } ); it( 'unicode support - combining mark multiple backward', () => { setData( model, '

foo̻̐ͩ[]bar

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

fo[o̻̐ͩ]bar

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'unicode support - combining mark to the end', '

f[o]o̻̐ͩ

', '

f[oo̻̐ͩ]

', { unit: 'word' } ); test( 'unicode support - surrogate pairs forward', '

[]foo\uD83D\uDCA9

', '

[foo\uD83D\uDCA9]

', { unit: 'word' } ); it( 'unicode support - surrogate pairs backward', () => { setData( model, '

foo\uD83D\uDCA9[]

' ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[foo\uD83D\uDCA9]

' ); expect( doc.selection.isBackward ).to.true; } ); function testStopCharacter( stopCharacter ) { describe( `stop character: "${ stopCharacter }"`, () => { test( 'extends whole word forward', `

f[]oo${ stopCharacter }bar

`, `

f[oo]${ stopCharacter }bar

`, { unit: 'word' } ); it( 'extends whole word backward to the previous word', () => { setData( model, `

foo${ stopCharacter }ba[]r

`, { lastRangeBackward: true } ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `

foo${ stopCharacter }[ba]r

` ); expect( doc.selection.isBackward ).to.true; } ); it( 'extends whole word backward', () => { setData( model, `

fo[]o${ stopCharacter }bar

`, { lastRangeBackward: true } ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `

[fo]o${ stopCharacter }bar

` ); expect( doc.selection.isBackward ).to.true; } ); test( 'ignores attributes when in one word - case 1', `

foo[]<$text bold="true">barbaz${ stopCharacter }foobarbaz

`, `

foo[<$text bold="true">barbaz]${ stopCharacter }foobarbaz

`, { unit: 'word' } ); test( 'ignores attributes when in one word - case 2', `

foo[]<$text bold="true">bar${ stopCharacter }foobarbaz

`, `

foo[<$text bold="true">bar]${ stopCharacter }foobarbaz

`, { unit: 'word' } ); test( 'ignores attributes when in one word - case 3', `

foo[]<$text bold="true">bar<$text italic="true">bazbaz${ stopCharacter }foobarbaz

`, `

foo[<$text bold="true">bar<$text italic="true">bazbaz]${ stopCharacter }foobarbaz

`, { unit: 'word' } ); it( 'extends whole word backward to the previous word ignoring attributes - case 1', () => { setData( model, `

foobarbaz${ stopCharacter }foo<$text bold="true">barbaz[]

` ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `

foobarbaz${ stopCharacter }[foo<$text bold="true">barbaz]

` ); expect( doc.selection.isBackward ).to.true; } ); it( 'extends whole word backward to the previous word ignoring attributes - case 2', () => { setData( model, `

foobarbaz${ stopCharacter }<$text bold="true">barbaz[]

` ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `

foobarbaz${ stopCharacter }[<$text bold="true">barbaz]

` ); expect( doc.selection.isBackward ).to.true; } ); } ); } } ); describe( 'beyond element', () => { test( 'extends over boundary of empty elements', '

[]

', '

[

]

', { unit: 'word' } ); it( 'extends over boundary of empty elements (backward)', () => { setData( model, '

[]

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[

]

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends over boundary of non-empty elements', '

a[]

bcd

', '

a[

]bcd

', { unit: 'word' } ); it( 'extends over boundary of non-empty elements (backward)', () => { setData( model, '

a

[]bcd

' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

a[

]bcd

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends over character after boundary', '

a[

]bcd

', '

a[

bcd]

', { unit: 'word' } ); it( 'extends over character after boundary (backward)', () => { setData( model, '

abc[

]d

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

ab[c

]d

' ); expect( doc.selection.isBackward ).to.true; } ); test( 'stops on the first position where text is allowed - inside block', '

a[]

bcd

', '

a[

]bcd

', { unit: 'word' } ); test( 'stops on the first position where text is allowed - inside inline element', '

a[

]bcdef

', '

a[

]bcdef

', { unit: 'word' } ); test( 'extends over element when next node is a text', '

a[]bc

', '

a[]bc

', { unit: 'word' } ); test( 'extends over element when next node is a text - backward', '

ab[]c

', '

ab[]c

', { unit: 'word', direction: 'backward' } ); it( 'shrinks over boundary of empty elements', () => { setData( model, '

[

]

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { unit: 'word' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[]

' ); expect( doc.selection.isBackward ).to.false; } ); it( 'shrinks over boundary of empty elements (backward)', () => { setData( model, '

[

]

' ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

[]

' ); expect( doc.selection.isBackward ).to.false; } ); it( 'shrinks over boundary of non-empty elements', () => { setData( model, '

a[

]b

', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { unit: 'word' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

a

[]b

' ); expect( doc.selection.isBackward ).to.false; } ); test( 'shrinks over boundary of non-empty elements (backward)', '

a[

]b

', '

a[]

b

', { unit: 'word', direction: 'backward' } ); it( 'updates selection attributes', () => { setData( model, '

<$text bold="true">foo[b]

' ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '

<$text bold="true">foo[]b

' ); expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true ); } ); } ); describe( 'beyond element – skipping incorrect positions', () => { beforeEach( () => { model.schema.register( 'quote' ); model.schema.extend( 'quote', { allowIn: '$root' } ); model.schema.extend( '$block', { allowIn: 'quote' } ); } ); test( 'skips position at the beginning of an element which does not allow text', '

x[]

y

z

', '

x[

]y

z

', { unit: 'word' } ); test( 'skips position at the end of an element which does not allow text - backward', '

x

y

[]z

', '

x

y[

]z

', { unit: 'word', direction: 'backward' } ); test( 'skips position at the end of an element which does not allow text', '

x[

y]

z

', '

x[

y

]z

', { unit: 'word' } ); test( 'skips position at the beginning of an element which does not allow text - backward', '

x

[]y

z

', '

x[

]y

z

', { unit: 'word', direction: 'backward' } ); test( 'extends to an empty block after skipping incorrect position', '

x[]

z

', '

x[

]

z

', { unit: 'word' } ); test( 'extends to an empty block after skipping incorrect position - backward', '

x

[]z

', '

x

[

]z

', { unit: 'word', direction: 'backward' } ); } ); } ); describe( 'objects handling', () => { beforeEach( () => { model.schema.register( 'obj', { isObject: true } ); model.schema.extend( 'obj', { allowIn: '$root' } ); model.schema.extend( '$text', { allowIn: 'obj' } ); model.schema.register( 'inlineObj', { allowIn: 'p', isObject: true } ); model.schema.extend( '$text', { allowIn: 'inlineObj' } ); } ); test( 'extends over next object element when at the end of an element', '

foo[]

bar', '

foo[

bar]' ); test( 'extends over previous object element when at the beginning of an element ', 'bar

[]foo

', '[bar

]foo

', { direction: 'backward' } ); test( 'extends over object elements - forward', '[]', '[]' ); it( 'extends over object elements - backward', () => { setData( model, '[]', { lastRangeBackward: true } ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '[]' ); expect( doc.selection.isBackward ).to.true; } ); test( 'extends over inline objects - forward', '

foo[]bar

', '

foo[bar]

' ); test( 'extends over inline objects - backward', '

bar[]foo

', '

[bar]foo

', { direction: 'backward' } ); test( 'extends over empty inline objects - forward', '

foo[]

', '

foo[]

' ); test( 'extends over empty inline objects - backward', '

[]foo

', '

[]foo

', { direction: 'backward' } ); } ); describe( 'limits handling', () => { beforeEach( () => { model.schema.register( 'inlineLimit', { isLimit: true } ); model.schema.extend( 'inlineLimit', { allowIn: '$block' } ); model.schema.extend( '$text', { allowIn: 'inlineLimit' } ); model.schema.register( 'blockLimit', { isLimit: true } ); model.schema.extend( 'blockLimit', { allowIn: '$root' } ); model.schema.extend( 'p', { allowIn: 'blockLimit' } ); } ); test( 'should not extend to outside of inline limit element', '

xfoo[]x

', '

xfoo[]x

' ); test( 'should not extend to outside of inline limit element - backward', '

x[]foox

', '

x[]foox

', { direction: 'backward' } ); test( 'should not extend to outside of block limit element', '

x

foo[]

x

', '

x

foo[]

x

' ); test( 'should not extend to outside of block limit element - backward', '

x

[]foo

x

', '

x

[]foo

x

', { direction: 'backward' } ); // This may seem counterintuitive but it makes sense. The limit element means // that it can't be left or modified from inside. If you want the same behavior from outside // register it as an object. test( 'should enter a limit element', '

foo[]

x

', '

foo[

]x

' ); test( 'should enter a limit element - backward', '

x

[]foo

', '

x[

]foo

', { direction: 'backward' } ); } ); } ); function test( title, input, output, options ) { it( title, () => { input = input.normalize(); output = output.normalize(); setData( model, input ); // Creating new instance of selection instead of operation on module:engine/model/document~Document#selection. // Document's selection will throw errors in some test cases (which are correct cases, but only for // non-document selections). const testSelection = new Selection( doc.selection ); modifySelection( model, testSelection, options ); expect( stringify( doc.getRoot(), testSelection ) ).to.equal( output ); } ); } } );