Skip to content

Commit

Permalink
Refactoring of triple click selection logic.
Browse files Browse the repository at this point in the history
  • Loading branch information
illia-stv committed Oct 5, 2023
1 parent 58b93f3 commit e4b2df6
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 38 deletions.
43 changes: 20 additions & 23 deletions packages/ckeditor5-widget/src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
type ViewDocumentFragment,
type ViewDocumentMouseDownEvent,
type ViewElement,
type Schema
type Schema,
type Position
} from '@ckeditor/ckeditor5-engine';

import { Delete, type ViewDocumentDeleteEvent } from '@ckeditor/ckeditor5-typing';
Expand Down Expand Up @@ -251,6 +252,7 @@ export default class Widget extends Plugin {
const editor = this.editor;
const model = editor.model;
const mapper = editor.editing.mapper;
const schema = model.schema;

const viewElement = mapper.findMappedViewAncestor( this.editor.editing.view.createPositionAt( element, 0 ) );
const modelElement = findTextBlockAncestor( mapper.toModelElement( viewElement )!, model.schema );
Expand All @@ -260,17 +262,16 @@ export default class Widget extends Plugin {
}

model.change( writer => {
const schema = model.schema;
const treeWalker = new TreeWalker( { startPosition: writer.createRangeOn( modelElement ).end } );
const nextTextBlock = findNextTextBlock( treeWalker, schema );

const start = writer.createRangeIn( modelElement ).start;
const end = nextTextBlock && nextTextBlock.is( 'element' ) &&
writer.createRangeIn( nextTextBlock ).start || writer.createRangeIn( modelElement ).end;
const nextTextBlock = !schema.isLimit( modelElement ) ?
findNextTextBlock( writer.createPositionAfter( modelElement ), schema ) :
null;

const range = writer.createRange( start, end );
const start = writer.createPositionAt( modelElement, 0 );
const end = nextTextBlock ?
writer.createPositionAt( nextTextBlock, 0 ) :
writer.createPositionAt( modelElement, 'end' );

writer.setSelection( range );
writer.setSelection( writer.createRange( start, end ) );
} );

return true;
Expand Down Expand Up @@ -528,21 +529,17 @@ function findTextBlockAncestor( modelElement: Element, schema: Schema ): Element
/**
* Returns next text block where could put selection.
*/
function findNextTextBlock( treeWalker: TreeWalker, schema: Schema ): Element | null {
const value = treeWalker.next();

if ( !value.value ) {
return null;
}
function findNextTextBlock( position: Position, schema: Schema ): Element | null {
const treeWalker = new TreeWalker( { startPosition: position } );

const item = value.value.item;

if ( !schema.isLimit( item ) && schema.checkChild( item, '$text' ) ) {
return item;
}
for ( const { item } of treeWalker ) {
if ( schema.isLimit( item ) || !item.is( 'element' ) ) {
return null;
}

if ( !schema.isBlock( item ) && !schema.isLimit( item ) ) {
return findNextTextBlock( treeWalker, schema );
if ( schema.checkChild( item, '$text' ) ) {
return item;
}
}

return null;
Expand Down
204 changes: 189 additions & 15 deletions packages/ckeditor5-widget/tests/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,13 +429,14 @@ describe( 'Widget', () => {
} );

describe( 'triple click', () => {
it( 'should properly set selection on entire paragraph', () => {
it( 'should set selection on entire paragraph', () => {
setModelData( model,
'<paragraph>[]foo bar</paragraph>'
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo bar</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 0 );
const paragraph = viewRoot.getChild( 1 );
const preventDefault = sinon.spy();
const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( paragraph ),
Expand All @@ -448,18 +449,46 @@ describe( 'Widget', () => {
sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo bar]</paragraph>'
);
} );

it( 'should properly set selection', () => {
it( 'should set selection on entire paragraph with attributes in text', () => {
setModelData( model,
'<paragraph>[]foo bar</paragraph>' +
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo <$text attr="true">bar</$text></paragraph>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 1 );
const bold = paragraph.getChild( 1 );
const preventDefault = sinon.spy();
const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( bold ),
preventDefault,
detail: 3
} );

viewDocument.fire( 'mousedown', domEventDataMock );

sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo <$text attr="true">bar</$text>]</paragraph>'
);
} );

it( 'should extend selection to include start of next text block', () => {
setModelData( model,
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo bar</paragraph>' +
'<paragraph>baz</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 0 );
const paragraph = viewRoot.getChild( 1 );
const preventDefault = sinon.spy();
const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( paragraph ),
Expand All @@ -472,19 +501,21 @@ describe( 'Widget', () => {
sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo bar</paragraph>' +
'<paragraph>]baz</paragraph>'
);
} );

it( 'should properly set selection when widget is after paragraph', () => {
it( 'should not extend selection to include following block widget', () => {
setModelData( model,
'<paragraph>[]foo bar</paragraph>' +
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo bar</paragraph>' +
'<widget><nested>foo bar</nested></widget>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 0 );
const paragraph = viewRoot.getChild( 1 );
const preventDefault = sinon.spy();
const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( paragraph ),
Expand All @@ -497,19 +528,21 @@ describe( 'Widget', () => {
sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo bar]</paragraph>' +
'<widget><nested>foo bar</nested></widget>'
);
} );

it( 'should properly set selection when block quote is after paragraph', () => {
it( 'should extend selection to include start of next text block in block quote', () => {
setModelData( model,
'<paragraph>[]foo bar</paragraph>' +
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo bar</paragraph>' +
'<blockQuote><paragraph>foo bar</paragraph></blockQuote>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 0 );
const paragraph = viewRoot.getChild( 1 );
const preventDefault = sinon.spy();
const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( paragraph ),
Expand All @@ -522,19 +555,21 @@ describe( 'Widget', () => {
sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo bar</paragraph>' +
'<blockQuote><paragraph>]foo bar</paragraph></blockQuote>'
);
} );

it( 'should properly set selection when after block quote is paragraph', () => {
it( 'should extend selection starting in block quote', () => {
setModelData( model,
'<blockQuote><paragraph>[]foo bar</paragraph></blockQuote>' +
'<paragraph>f[o]o</paragraph>' +
'<blockQuote><paragraph>foo bar</paragraph></blockQuote>' +
'<paragraph>foo bar</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const blockQuote = viewRoot.getChild( 0 );
const blockQuote = viewRoot.getChild( 1 );
const paragraph = blockQuote.getChild( 0 );
const preventDefault = sinon.spy();

Expand All @@ -549,10 +584,149 @@ describe( 'Widget', () => {
sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<blockQuote><paragraph>[foo bar</paragraph></blockQuote>' +
'<paragraph>]foo bar</paragraph>'
);
} );

it( 'should set selection on entire parqgraph with inline inside', () => {
setModelData( model,
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo<inline></inline>bar</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 1 );
const preventDefault = sinon.spy();

const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( paragraph ),
preventDefault,
detail: 3
} );

viewDocument.fire( 'mousedown', domEventDataMock );

sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo<inline></inline>bar]</paragraph>'
);
} );

it( 'should extend selection with inline in paragraph', () => {
setModelData( model,
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo<inline></inline>bar</paragraph>' +
'<paragraph>foo</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 1 );
const preventDefault = sinon.spy();

const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( paragraph ),
preventDefault,
detail: 3
} );

viewDocument.fire( 'mousedown', domEventDataMock );

sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo<inline></inline>bar</paragraph>' +
'<paragraph>]foo</paragraph>'
);
} );

it( 'should extend selection with inline in paragraph (when inline widget clicked)', () => {
setModelData( model,
'<paragraph>f[o]o</paragraph>' +
'<paragraph>foo<inline></inline>bar</paragraph>' +
'<paragraph>foo</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const paragraph = viewRoot.getChild( 1 );
const inline = paragraph.getChild( 1 );
const preventDefault = sinon.spy();

const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( inline ),
preventDefault,
detail: 3
} );

viewDocument.fire( 'mousedown', domEventDataMock );

sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<paragraph>[foo<inline></inline>bar</paragraph>' +
'<paragraph>]foo</paragraph>'
);
} );

it( 'should not extend selection inside widget', () => {
setModelData( model,
'<paragraph>f[o]o</paragraph>' +
'<widget><nested>foo bar</nested></widget>' +
'<paragraph>foo bar</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const widget = viewRoot.getChild( 1 );
const nested = widget.getChild( 0 );
const preventDefault = sinon.spy();
const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( nested ),
preventDefault,
detail: 3
} );

viewDocument.fire( 'mousedown', domEventDataMock );

sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<widget><nested>[foo bar]</nested></widget>' +
'<paragraph>foo bar</paragraph>'
);
} );

it( 'should not extend selection outside limit element', () => {
setModelData( model,
'<paragraph>f[o]o</paragraph>' +
'<widget>foo</widget>' +
'<paragraph>foo bar</paragraph>'
);

const viewRoot = viewDocument.getRoot();
const widget = viewRoot.getChild( 1 );
const preventDefault = sinon.spy();
const domEventDataMock = new DomEventData( view, {
target: view.domConverter.mapViewToDom( widget ),
preventDefault,
detail: 3
} );

viewDocument.fire( 'mousedown', domEventDataMock );

sinon.assert.called( preventDefault );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo</paragraph>' +
'<widget>[foo]</widget>' +
'<paragraph>foo bar</paragraph>'
);
} );
} );

describe( 'keys handling', () => {
Expand Down

0 comments on commit e4b2df6

Please sign in to comment.