/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /* globals console */ import Model from '../../src/model/model'; import Batch from '../../src/model/batch'; import Element from '../../src/model/element'; import Text from '../../src/model/text'; import Range from '../../src/model/range'; import Position from '../../src/model/position'; import LiveRange from '../../src/model/liverange'; import DocumentSelection from '../../src/model/documentselection'; import InsertOperation from '../../src/model/operation/insertoperation'; import MoveOperation from '../../src/model/operation/moveoperation'; import AttributeOperation from '../../src/model/operation/attributeoperation'; import SplitOperation from '../../src/model/operation/splitoperation'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; import count from '@ckeditor/ckeditor5-utils/src/count'; import { setData, getData } from '../../src/dev-utils/model'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils'; describe( 'DocumentSelection', () => { let model, doc, root, selection, liveRange, range; const fooStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'foo' ); const abcStoreAttrKey = DocumentSelection._getStoreAttributeKey( 'abc' ); beforeEach( () => { model = new Model(); doc = model.document; root = doc.createRoot(); root._appendChild( [ new Element( 'p' ), new Element( 'p' ), new Element( 'p', [], new Text( 'foobar' ) ), new Element( 'p' ), new Element( 'p' ), new Element( 'p' ), new Element( 'p', [], new Text( 'foobar' ) ) ] ); selection = doc.selection; model.schema.register( 'p', { inheritAllFrom: '$block' } ); model.schema.register( 'widget', { isObject: true } ); liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ); range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 2, 2 ] ) ); } ); afterEach( () => { sinon.restore(); model.destroy(); liveRange.detach(); } ); describe( 'default range', () => { it( 'should go to the first editable element', () => { const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 1 ); expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true; expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true; expect( selection ).to.have.property( 'isBackward', false ); } ); it( 'should be set to the beginning of the doc if there is no editable element', () => { model = new Model(); doc = model.document; root = doc.createRoot(); root._insertChild( 0, new Text( 'foobar' ) ); selection = doc.selection; const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 1 ); expect( selection.anchor.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true; expect( selection.focus.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true; expect( selection ).to.have.property( 'isBackward', false ); expect( count( selection.getAttributes() ) ).to.equal( 0 ); } ); it( 'should skip element when you can not put selection', () => { model = new Model(); doc = model.document; root = doc.createRoot(); root._insertChild( 0, [ new Element( 'img' ), new Element( 'p', [], new Text( 'foobar' ) ) ] ); model.schema.register( 'img' ); model.schema.register( 'p', { inheritAllFrom: '$block' } ); selection = doc.selection; const ranges = Array.from( selection.getRanges() ); expect( ranges.length ).to.equal( 1 ); expect( selection.anchor.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true; expect( selection.focus.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true; expect( selection ).to.have.property( 'isBackward', false ); expect( count( selection.getAttributes() ) ).to.equal( 0 ); } ); } ); describe( 'isCollapsed', () => { it( 'should be true for the default range (in text position)', () => { expect( selection.isCollapsed ).to.be.true; } ); it( 'should be false for the default range (object selection) ', () => { root._insertChild( 0, new Element( 'widget' ) ); expect( selection.isCollapsed ).to.be.false; } ); it( 'should back off to the default algorithm if selection has ranges', () => { selection._setTo( range ); expect( selection.isCollapsed ).to.be.false; } ); } ); describe( 'anchor', () => { it( 'should equal the default range\'s start (in text position)', () => { const expectedPos = new Position( root, [ 0, 0 ] ); expect( selection.anchor.isEqual( expectedPos ) ).to.be.true; } ); it( 'should equal the default range\'s start (object selection)', () => { root._insertChild( 0, new Element( 'widget' ) ); const expectedPos = new Position( root, [ 0 ] ); expect( selection.anchor.isEqual( expectedPos ) ).to.be.true; } ); it( 'should back off to the default algorithm if selection has ranges', () => { selection._setTo( range ); expect( selection.anchor.isEqual( range.start ) ).to.be.true; } ); } ); describe( 'focus', () => { it( 'should equal the default range\'s end (in text position)', () => { const expectedPos = new Position( root, [ 0, 0 ] ); expect( selection.focus.isEqual( expectedPos ) ).to.be.true; } ); it( 'should equal the default range\'s end (object selection)', () => { root._insertChild( 0, new Element( 'widget' ) ); const expectedPos = new Position( root, [ 1 ] ); expect( selection.focus.isEqual( expectedPos ) ).to.be.true; expect( selection.focus.isEqual( selection.getFirstRange().end ) ).to.be.true; } ); it( 'should back off to the default algorithm if selection has ranges', () => { selection._setTo( range ); expect( selection.focus.isEqual( range.end ) ).to.be.true; } ); } ); describe( 'rangeCount', () => { it( 'should return proper range count', () => { expect( selection.rangeCount ).to.equal( 1 ); selection._setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ); expect( selection.rangeCount ).to.equal( 1 ); selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ), new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] ); expect( selection.rangeCount ).to.equal( 2 ); } ); } ); describe( 'hasOwnRange', () => { it( 'should return true if selection has any ranges set', () => { selection._setTo( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ); expect( selection.hasOwnRange ).to.be.true; } ); it( 'should return false if selection has a default range', () => { expect( selection.hasOwnRange ).to.be.false; } ); } ); describe( 'markers', () => { it( 'should implement #markers collection', () => { expect( selection.markers ).to.instanceof( Collection ); expect( selection.markers ).to.length( 0 ); } ); it( 'should add markers to the collection when selection is inside the marker range', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 2 ] ), writer.createPositionFromPath( root, [ 2, 4 ] ) ) ); writer.addMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 0, 0 ] ), writer.createPositionFromPath( root, [ 2, 2 ] ) ), usingOperation: false } ); writer.addMarker( 'marker-2', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 2 ] ), writer.createPositionFromPath( root, [ 2, 4 ] ) ), usingOperation: false } ); writer.addMarker( 'marker-3', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 5 ] ) ), usingOperation: false } ); writer.addMarker( 'marker-4', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 4 ] ), writer.createPositionFromPath( root, [ 3, 0 ] ) ), usingOperation: false } ); } ); expect( selection.markers.map( marker => marker.name ) ).to.have.members( [ 'marker-2', 'marker-3' ] ); } ); it( 'should update markers after selection change', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 2 ] ) ) ); writer.addMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 6 ] ) ), usingOperation: false } ); writer.addMarker( 'marker-2', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 3 ] ) ), usingOperation: false } ); writer.addMarker( 'marker-3', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 3 ] ), writer.createPositionFromPath( root, [ 2, 6 ] ) ), usingOperation: false } ); } ); expect( selection.markers.map( marker => marker.name ) ).to.have.members( [ 'marker-1', 'marker-2' ] ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 4 ] ), writer.createPositionFromPath( root, [ 2, 5 ] ) ) ); } ); expect( selection.markers.map( marker => marker.name ) ).to.have.members( [ 'marker-1', 'marker-3' ] ); } ); it( 'should update markers after markers change', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 2 ] ) ) ); writer.addMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 6 ] ) ), usingOperation: false } ); writer.addMarker( 'marker-2', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 3 ] ) ), usingOperation: false } ); writer.addMarker( 'marker-3', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 3 ] ), writer.createPositionFromPath( root, [ 2, 6 ] ) ), usingOperation: false } ); } ); expect( selection.markers.map( marker => marker.name ), 1 ).to.have.members( [ 'marker-1', 'marker-2' ] ); model.change( writer => { writer.removeMarker( 'marker-1' ); writer.updateMarker( 'marker-2', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 3 ] ), writer.createPositionFromPath( root, [ 2, 6 ] ) ), usingOperation: false } ); writer.updateMarker( 'marker-3', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 3 ] ) ), usingOperation: false } ); } ); expect( selection.markers.map( marker => marker.name ), 2 ).to.have.members( [ 'marker-3' ] ); } ); it( 'should not add marker when collapsed selection is on the marker left bound', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 2 ] ), writer.createPositionFromPath( root, [ 2, 4 ] ) ) ); writer.addMarker( 'marker', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 2 ] ) ), usingOperation: false } ); } ); expect( selection.markers ).to.length( 0 ); } ); it( 'should not add marker when collapsed selection is on the marker right bound', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 4 ] ) ) ); writer.addMarker( 'marker', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 2 ] ), writer.createPositionFromPath( root, [ 2, 4 ] ) ), usingOperation: false } ); } ); expect( selection.markers ).to.length( 0 ); } ); it( 'should add marker when non-collapsed selection is inside a marker and touches the left bound', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 3 ] ) ) ); writer.addMarker( 'marker', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 5 ] ) ), usingOperation: false } ); } ); expect( selection.markers.map( marker => marker.name ) ).to.have.members( [ 'marker' ] ); } ); it( 'should add marker when non-collapsed selection is inside a marker and touches the right bound', () => { model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 2 ] ), writer.createPositionFromPath( root, [ 2, 5 ] ) ) ); writer.addMarker( 'marker', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 5 ] ) ), usingOperation: false } ); } ); expect( selection.markers.map( marker => marker.name ) ).to.have.members( [ 'marker' ] ); } ); it( 'should add marker of selected widget', () => { root._insertChild( 0, new Element( 'widget' ) ); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 0 ] ), writer.createPositionFromPath( root, [ 1 ] ) ) ); writer.addMarker( 'marker', { range: writer.createRange( writer.createPositionFromPath( root, [ 0 ] ), writer.createPositionFromPath( root, [ 1 ] ) ), usingOperation: false } ); } ); expect( selection.markers.map( marker => marker.name ) ).to.have.members( [ 'marker' ] ); } ); describe( 'should fire change:marker event when', () => { // Set marker to range 0-4. beforeEach( () => { model.change( writer => { writer.addMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 4 ] ) ), usingOperation: false } ); } ); } ); it( 'selection ranges change (marker added to the selection)', () => { const spy = sinon.spy(); model.change( writer => { // The selection has no markers before the change. model.document.selection.on( 'change:marker', ( evt, data ) => { expect( data.oldMarkers ).to.deep.equal( [] ); spy(); } ); // Move selection to 1-2, that is inside 0-4 marker. writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 2 ] ) ) ); } ); expect( spy.calledOnce ).to.be.true; } ); it( 'selection ranges change (marker removed from the selection)', () => { const spy = sinon.spy(); model.change( writer => { writer.setSelection( writer.createRange( writer.createPositionFromPath( root, [ 2, 1 ] ), writer.createPositionFromPath( root, [ 2, 2 ] ) ) ); // The selection is in a marker before the change. model.document.selection.on( 'change:marker', ( evt, data ) => { expect( data.oldMarkers.map( marker => marker.name ) ).to.deep.equal( [ 'marker-1' ] ); spy(); } ); // Move the selection out of the marker. writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) ); } ); expect( spy.calledOnce ).to.be.true; } ); it( 'selection focus changes (marker removed from the selection)', () => { const spy = sinon.spy(); model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) ); // The selection is in a marker before the change. model.document.selection.on( 'change:marker', ( evt, data ) => { expect( data.oldMarkers.map( marker => marker.name ) ).to.deep.equal( [ 'marker-1' ] ); spy(); } ); // Move the selection focus out of the marker. writer.setSelectionFocus( writer.createPositionFromPath( root, [ 2, 5 ] ) ); } ); expect( spy.calledOnce ).to.be.true; } ); it( 'a new marker contains the selection', () => { const spy = sinon.spy(); model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) ); // The selection is not in a marker before the change. model.document.selection.on( 'change:marker', ( evt, data ) => { expect( data.oldMarkers ).to.deep.equal( [] ); spy(); } ); writer.updateMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 6 ] ) ) } ); } ); expect( spy.calledOnce ).to.be.true; } ); it( 'a marker stops contains the selection', () => { const spy = sinon.spy(); model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 3 ] ) ); // The selection is in a marker before the change. model.document.selection.on( 'change:marker', ( evt, data ) => { expect( data.oldMarkers.map( marker => marker.name ) ).to.deep.equal( [ 'marker-1' ] ); spy(); } ); writer.updateMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 1 ] ) ) } ); } ); expect( spy.calledOnce ).to.be.true; } ); } ); describe( 'should not fire change:marker event when', () => { // Set marker to range 0-4. beforeEach( () => { model.change( writer => { writer.addMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 4 ] ) ), usingOperation: false } ); } ); } ); it( 'selection ranges change does not change selection markers (no markers)', () => { const spy = sinon.spy(); model.document.selection.on( 'change:marker', spy ); model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) ); } ); expect( spy.called ).to.be.false; } ); it( 'selection ranges change does not change selection markers (same markers)', () => { model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) ); } ); const spy = sinon.spy(); model.document.selection.on( 'change:marker', spy ); model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 3 ] ) ); } ); expect( spy.called ).to.be.false; } ); it( 'selection focus change does not change selection markers', () => { model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) ); } ); const spy = sinon.spy(); model.document.selection.on( 'change:marker', spy ); model.change( writer => { writer.setSelectionFocus( writer.createPositionFromPath( root, [ 2, 3 ] ) ); } ); expect( spy.called ).to.be.false; } ); it( 'changed marker still contains the selection', () => { model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 2 ] ) ); } ); const spy = sinon.spy(); model.document.selection.on( 'change:marker', spy ); model.change( writer => { writer.updateMarker( 'marker-1', { range: writer.createRange( writer.createPositionFromPath( root, [ 2, 0 ] ), writer.createPositionFromPath( root, [ 2, 5 ] ) ) } ); } ); expect( spy.called ).to.be.false; } ); it( 'removed marker did not contain the selection', () => { model.change( writer => { writer.setSelection( writer.createPositionFromPath( root, [ 2, 5 ] ) ); } ); const spy = sinon.spy(); model.document.selection.on( 'change:marker', spy ); model.change( writer => { writer.removeMarker( 'marker-1' ); } ); expect( spy.called ).to.be.false; } ); } ); } ); describe( 'destroy()', () => { it( 'should unbind all events', () => { selection._setTo( [ range, liveRange ] ); const ranges = Array.from( selection._selection._ranges ); sinon.spy( ranges[ 0 ], 'detach' ); sinon.spy( ranges[ 1 ], 'detach' ); selection.destroy(); expect( ranges[ 0 ].detach.called ).to.be.true; expect( ranges[ 1 ].detach.called ).to.be.true; } ); } ); describe( 'getFirstRange()', () => { it( 'should return default range if no ranges were added', () => { const firstRange = selection.getFirstRange(); expect( firstRange.start.isEqual( new Position( root, [ 0, 0 ] ) ) ); expect( firstRange.end.isEqual( new Position( root, [ 0, 0 ] ) ) ); } ); } ); describe( 'getLastRange()', () => { it( 'should return default range if no ranges were added', () => { const lastRange = selection.getLastRange(); expect( lastRange.start.isEqual( new Position( root, [ 0, 0 ] ) ) ); expect( lastRange.end.isEqual( new Position( root, [ 0, 0 ] ) ) ); } ); } ); describe( 'getSelectedElement()', () => { it( 'should return selected element', () => { selection._setTo( liveRange ); const p = root.getChild( 0 ); expect( selection.getSelectedElement() ).to.equal( p ); } ); } ); describe( 'is()', () => { it( 'should return true for selection', () => { expect( selection.is( 'selection' ) ).to.be.true; expect( selection.is( 'model:selection' ) ).to.be.true; } ); it( 'should return true for documentSelection', () => { expect( selection.is( 'documentSelection' ) ).to.be.true; expect( selection.is( 'model:documentSelection' ) ).to.be.true; } ); it( 'should return false for other values', () => { expect( selection.is( 'node' ) ).to.be.false; expect( selection.is( 'model:node' ) ).to.be.false; expect( selection.is( 'text' ) ).to.be.false; expect( selection.is( 'textProxy' ) ).to.be.false; expect( selection.is( 'element' ) ).to.be.false; expect( selection.is( 'element', 'paragraph' ) ).to.be.false; expect( selection.is( 'rootElement' ) ).to.be.false; expect( selection.is( 'view:selection' ) ).to.be.false; expect( selection.is( 'view:documentSelection' ) ).to.be.false; } ); } ); describe( '_setTo() - set collapsed at', () => { it( 'detaches all existing ranges', () => { selection._setTo( [ range, liveRange ] ); const spy = sinon.spy( LiveRange.prototype, 'detach' ); selection._setTo( root, 0 ); expect( spy.calledTwice ).to.be.true; } ); } ); describe( '_setFocus()', () => { it( 'modifies default range', () => { const startPos = selection.getFirstPosition(); const endPos = Position._createAt( root, 'end' ); selection._setFocus( endPos ); expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' ); expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' ); } ); it( 'detaches the range it replaces', () => { const startPos = Position._createAt( root, 1 ); const endPos = Position._createAt( root, 2 ); const newEndPos = Position._createAt( root, 4 ); const spy = sinon.spy( LiveRange.prototype, 'detach' ); selection._setTo( new Range( startPos, endPos ) ); selection._setFocus( newEndPos ); expect( spy.calledOnce ).to.be.true; } ); it( 'refreshes attributes', () => { const spy = sinon.spy( selection._selection, '_updateAttributes' ); selection._setFocus( Position._createAt( root, 1 ) ); expect( spy.called ).to.be.true; } ); } ); describe( '_setTo() - remove all ranges', () => { let spy, ranges; beforeEach( () => { selection._setTo( [ liveRange, range ] ); spy = sinon.spy(); selection.on( 'change:range', spy ); ranges = Array.from( selection._selection._ranges ); sinon.spy( ranges[ 0 ], 'detach' ); sinon.spy( ranges[ 1 ], 'detach' ); selection._setTo( null ); } ); it( 'should remove all stored ranges (and reset to default range)', () => { expect( Array.from( selection.getRanges() ).length ).to.equal( 1 ); expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true; expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true; } ); it( 'should detach ranges', () => { expect( ranges[ 0 ].detach.called ).to.be.true; expect( ranges[ 1 ].detach.called ).to.be.true; } ); it( 'should refresh attributes', () => { const spy = sinon.spy( selection._selection, '_updateAttributes' ); selection._setTo( null ); expect( spy.called ).to.be.true; } ); } ); describe( '_setTo()', () => { it( 'should throw an error when range is invalid', () => { expectToThrowCKEditorError( () => { selection._setTo( [ { invalid: 'range' } ] ); }, /model-selection-set-ranges-not-range/, model ); } ); it( 'should do nothing when trying to set selection to the graveyard', () => { // Catches the 'Trying to add a Range that is in the graveyard root. Range rejected.' warning in the CK_DEBUG mode. sinon.stub( console, 'warn' ); const range = new Range( new Position( model.document.graveyard, [ 0 ] ) ); selection._setTo( range ); expect( selection._ranges ).to.deep.equal( [] ); } ); it( 'should detach removed ranges', () => { selection._setTo( [ liveRange, range ] ); const oldRanges = Array.from( selection._selection._ranges ); sinon.spy( oldRanges[ 0 ], 'detach' ); sinon.spy( oldRanges[ 1 ], 'detach' ); selection._setTo( [] ); expect( oldRanges[ 0 ].detach.called ).to.be.true; expect( oldRanges[ 1 ].detach.called ).to.be.true; } ); it( 'should refresh attributes', () => { const spy = sinon.spy( selection._selection, '_updateAttributes' ); selection._setTo( [ range ] ); expect( spy.called ).to.be.true; } ); // See #630. it( 'should refresh attributes – integration test for #630', () => { model.schema.extend( '$text', { allowIn: '$root' } ); setData( model, 'f<$text italic="true">[o$text><$text bold="true">ob]a$text>r' ); selection._setTo( [ Range._createFromPositionAndShift( selection.getLastRange().end, 0 ) ] ); expect( selection.getAttribute( 'bold' ) ).to.equal( true ); expect( selection.hasAttribute( 'italic' ) ).to.equal( false ); expect( getData( model ) ) .to.equal( 'f<$text italic="true">o$text><$text bold="true">ob[]a$text>r' ); } ); } ); describe( '_isStoreAttributeKey', () => { it( 'should return true if given key is a key of an attribute stored in element by DocumentSelection', () => { expect( DocumentSelection._isStoreAttributeKey( fooStoreAttrKey ) ).to.be.true; } ); it( 'should return false if given key is not a key of an attribute stored in element by DocumentSelection', () => { expect( DocumentSelection._isStoreAttributeKey( 'foo' ) ).to.be.false; } ); } ); describe( 'attributes', () => { let fullP, emptyP, rangeInFullP, rangeInEmptyP; beforeEach( () => { root._removeChildren( 0, root.childCount ); root._appendChild( [ new Element( 'p', [], new Text( 'foobar' ) ), new Element( 'p', [], [] ) ] ); fullP = root.getChild( 0 ); emptyP = root.getChild( 1 ); rangeInFullP = new Range( new Position( root, [ 0, 4 ] ), new Position( root, [ 0, 4 ] ) ); rangeInEmptyP = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ); // I've lost 30 mins debugging why my tests fail and that was due to the above code reusing // a root which wasn't empty (so the ranges didn't actually make much sense). expect( root.childCount ).to.equal( 2 ); } ); describe( '_setAttribute()', () => { it( 'should set attribute', () => { selection._setTo( [ rangeInEmptyP ] ); selection._setAttribute( 'foo', 'bar' ); expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' ); } ); } ); describe( '_removeAttribute()', () => { it( 'should remove attribute set on the text fragment', () => { selection._setTo( [ rangeInFullP ] ); selection._setAttribute( 'foo', 'bar' ); selection._removeAttribute( 'foo' ); expect( selection.getAttribute( 'foo' ) ).to.be.undefined; } ); it( 'should prevent auto update of the attribute even if attribute is not preset yet', () => { selection._setTo( new Position( root, [ 0, 1 ] ) ); // Remove "foo" attribute that is not present in selection yet. expect( selection.hasAttribute( 'foo' ) ).to.be.false; selection._removeAttribute( 'foo' ); // Trigger selecton auto update on document change. It should not get attribute from surrounding text; model.change( writer => { writer.setAttribute( 'foo', 'bar', Range._createIn( fullP ) ); } ); expect( selection.getAttribute( 'foo' ) ).to.be.undefined; } ); } ); describe( '_getStoredAttributes()', () => { it( 'should return no values if there are no ranges in selection', () => { const values = Array.from( selection._getStoredAttributes() ); expect( values ).to.deep.equal( [] ); } ); } ); describe( 'are updated on a direct range change', () => { beforeEach( () => { root._insertChild( 0, [ new Element( 'p', { p: true } ), new Text( 'a', { a: true } ), new Element( 'p', { p: true } ), new Text( 'b', { b: true } ), new Text( 'c', { c: true } ), new Element( 'p', [], [ new Text( 'd', { d: true } ) ] ), new Element( 'p', { p: true } ), new Text( 'e', { e: true } ) ] ); } ); it( 'if selection is a range, should find first character in it and copy it\'s attributes', () => { selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] ); // Step into elements when looking for first character: selection._setTo( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] ); } ); it( 'if selection is collapsed it should seek a character to copy that character\'s attributes', () => { // Take styles from character before selection. selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] ); // If there are none, // Take styles from character after selection. selection._setTo( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] ); // If there are none, // Look from the selection position to the beginning of node looking for character to take attributes from. selection._setTo( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'c', true ] ] ); // If there are none, // Look from the selection position to the end of node looking for character to take attributes from. selection._setTo( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] ); // If there are no characters to copy attributes from, use stored attributes. selection._setTo( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [] ); } ); it( 'should overwrite any previously set attributes', () => { selection._setTo( new Position( root, [ 5, 0 ] ) ); selection._setAttribute( 'x', true ); selection._setAttribute( 'y', true ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ], [ 'x', true ], [ 'y', true ] ] ); selection._setTo( new Position( root, [ 1 ] ) ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] ); } ); it( 'should fire change:attribute event', () => { const spy = sinon.spy(); selection.on( 'change:attribute', spy ); selection._setTo( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] ); expect( spy.calledOnce ).to.be.true; } ); it( 'should not fire change:attribute event if attributes did not change', () => { selection._setTo( new Position( root, [ 5, 0 ] ) ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] ); const spy = sinon.spy(); selection.on( 'change:attribute', spy ); selection._setTo( new Position( root, [ 5, 1 ] ) ); expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] ); expect( spy.called ).to.be.false; } ); } ); // #986 describe( 'are not inherited from the inside of object elements', () => { beforeEach( () => { model.schema.register( 'image', { isObject: true } ); model.schema.extend( 'image', { allowIn: '$root' } ); model.schema.extend( 'image', { allowIn: '$block' } ); model.schema.register( 'caption' ); model.schema.extend( 'caption', { allowIn: 'image' } ); model.schema.extend( '$text', { allowIn: [ 'image', 'caption' ], allowAttributes: 'bold' } ); } ); it( 'ignores attributes inside an object if selection contains that object', () => { setData( model, '
[
[
foo
[foo
' ); expect( selection.hasAttribute( 'bold' ) ).to.equal( false ); } ); it( 'reads attributes from text even if the selection contains an object', () => { setData( model, 'x<$text bold="true">[bar$text>
[