/** * @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[]
a[
]
a[
]
a[
ab
ab[
[
]
', { 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$text>[b]
' ); modifySelection( model, doc.selection, { direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<$text bold="true">foo[]$text>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">bar$text>baz${ stopCharacter }foobarbaz
`, `foo[<$text bold="true">bar$text>baz]${ stopCharacter }foobarbaz
`, { unit: 'word' } ); test( 'ignores attributes when in one word - case 2', `foo[]<$text bold="true">bar$text>${ stopCharacter }foobarbaz
`, `foo[<$text bold="true">bar$text>]${ stopCharacter }foobarbaz
`, { unit: 'word' } ); test( 'ignores attributes when in one word - case 3', `foo[]<$text bold="true">bar$text><$text italic="true">baz$text>baz${ stopCharacter }foobarbaz
`, `foo[<$text bold="true">bar$text><$text italic="true">baz$text>baz]${ 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">bar$text>baz[]
` ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `foobarbaz${ stopCharacter }[foo<$text bold="true">bar$text>baz]
` ); 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">bar$text>baz[]
` ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( `foobarbaz${ stopCharacter }[<$text bold="true">bar$text>baz]
` ); 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[]
a[
]
a[
]
a[
ab
ab[
[
]
', { 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$text>[b]
' ); modifySelection( model, doc.selection, { unit: 'word', direction: 'backward' } ); expect( stringify( doc.getRoot(), doc.selection ) ).to.equal( '<$text bold="true">foo[]$text>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[]
foo[
[]foo
', '[]foo
', { direction: 'backward' } ); test( 'extends over object elements - forward', '[foo[]
foo[
[
foo[]
foo[
[
x
x
x
x
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
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 ); } ); } } );