' +
'
' +
diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
index 06a50eac5b7..d4d8bc445fa 100644
--- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
+++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
@@ -5,19 +5,20 @@
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
-import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
import Widget from '../../src/widget';
import WidgetTypeAround from '../../src/widgettypearound/widgettypearound';
import { toWidget } from '../../src/utils';
import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
describe( 'WidgetTypeAround', () => {
- let element, plugin, editor, editingView, viewDocument, viewRoot;
+ let element, plugin, editor, editingView, viewDocument, modelRoot, viewRoot;
beforeEach( async () => {
element = global.document.createElement( 'div' );
@@ -37,6 +38,7 @@ describe( 'WidgetTypeAround', () => {
editingView = editor.editing.view;
viewDocument = editingView.document;
viewRoot = viewDocument.getRoot();
+ modelRoot = editor.model.document.getRoot();
plugin = editor.plugins.get( WidgetTypeAround );
} );
@@ -47,12 +49,12 @@ describe( 'WidgetTypeAround', () => {
} );
describe( 'plugin', () => {
- it( 'is loaded', () => {
+ it( 'should be loaded', () => {
expect( editor.plugins.get( WidgetTypeAround ) ).to.be.instanceOf( WidgetTypeAround );
} );
- it( 'requires the Paragraph plugin', () => {
- expect( WidgetTypeAround.requires ).to.deep.equal( [ Paragraph ] );
+ it( 'should have a name', () => {
+ expect( WidgetTypeAround.pluginName ).to.equal( 'WidgetTypeAround' );
} );
} );
@@ -66,10 +68,10 @@ describe( 'WidgetTypeAround', () => {
it( 'should execute the "insertParagraph" command when inserting a paragraph before the widget', () => {
setModelData( editor.model, '
' );
- plugin._insertParagraph( viewRoot.getChild( 0 ), 'before' );
+ plugin._insertParagraph( modelRoot.getChild( 0 ), 'before' );
const spyExecutePosition = executeSpy.firstCall.args[ 1 ].position;
- const positionBeforeWidget = editor.model.createPositionBefore( editor.model.document.getRoot().getChild( 0 ) );
+ const positionBeforeWidget = editor.model.createPositionBefore( modelRoot.getChild( 0 ) );
sinon.assert.calledOnce( executeSpy );
sinon.assert.calledWith( executeSpy, 'insertParagraph' );
@@ -82,10 +84,10 @@ describe( 'WidgetTypeAround', () => {
it( 'should execute the "insertParagraph" command when inserting a paragraph after the widget', () => {
setModelData( editor.model, '
' );
- plugin._insertParagraph( viewRoot.getChild( 0 ), 'after' );
+ plugin._insertParagraph( modelRoot.getChild( 0 ), 'after' );
const spyExecutePosition = executeSpy.firstCall.args[ 1 ].position;
- const positionAfterWidget = editor.model.createPositionAfter( editor.model.document.getRoot().getChild( 0 ) );
+ const positionAfterWidget = editor.model.createPositionAfter( modelRoot.getChild( 0 ) );
sinon.assert.calledOnce( executeSpy );
sinon.assert.calledWith( executeSpy, 'insertParagraph' );
@@ -100,7 +102,7 @@ describe( 'WidgetTypeAround', () => {
setModelData( editor.model, '
' );
- plugin._insertParagraph( viewRoot.getChild( 0 ), 'after' );
+ plugin._insertParagraph( modelRoot.getChild( 0 ), 'after' );
sinon.assert.calledOnce( spy );
} );
@@ -110,7 +112,7 @@ describe( 'WidgetTypeAround', () => {
setModelData( editor.model, '
' );
- plugin._insertParagraph( viewRoot.getChild( 0 ), 'after' );
+ plugin._insertParagraph( modelRoot.getChild( 0 ), 'after' );
sinon.assert.calledOnce( spy );
} );
@@ -165,6 +167,21 @@ describe( 'WidgetTypeAround', () => {
expect( domWrapper.querySelectorAll( '.ck-widget__type-around__button' ) ).to.have.length( 2 );
} );
+ it( 'should inject a fake caret into the wrapper', () => {
+ setModelData( editor.model, '
' );
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.getChild( 1 ).is( 'uiElement' ) ).to.be.true;
+ expect( viewWidget.getChild( 1 ).hasClass( 'ck' ) ).to.be.true;
+ expect( viewWidget.getChild( 1 ).hasClass( 'ck-reset_all' ) ).to.be.true;
+ expect( viewWidget.getChild( 1 ).hasClass( 'ck-widget__type-around' ) ).to.be.true;
+
+ const domWrapper = editingView.domConverter.viewToDom( viewWidget.getChild( 1 ) );
+
+ expect( domWrapper.querySelectorAll( '.ck-widget__type-around__fake-caret' ) ).to.have.length( 1 );
+ } );
+
describe( 'UI button to type around', () => {
let buttonBefore, buttonAfter;
@@ -174,8 +191,8 @@ describe( 'WidgetTypeAround', () => {
const viewWidget = viewRoot.getChild( 0 );
const domWrapper = editingView.domConverter.viewToDom( viewWidget.getChild( 1 ) );
- buttonBefore = domWrapper.firstChild;
- buttonAfter = domWrapper.lastChild;
+ buttonBefore = domWrapper.children[ 0 ];
+ buttonAfter = domWrapper.children[ 1 ];
} );
it( 'should have proper CSS classes', () => {
@@ -209,10 +226,24 @@ describe( 'WidgetTypeAround', () => {
viewDocument.fire( eventInfo, domEventDataMock );
sinon.assert.calledOnce( typeAroundSpy );
- sinon.assert.calledWithExactly( typeAroundSpy, viewRoot.getChild( 1 ), 'before' );
+ sinon.assert.calledWithExactly( typeAroundSpy, modelRoot.getChild( 1 ), 'before' );
sinon.assert.calledOnce( preventDefaultSpy );
sinon.assert.calledOnce( stopSpy );
} );
+
+ it( 'should not cause WidgetTypeAround#_insertParagraph() when clicked something other than the button', () => {
+ const typeAroundSpy = sinon.spy( plugin, '_insertParagraph' );
+
+ const eventInfo = new EventInfo( viewDocument, 'mousedown' );
+ const domEventDataMock = new DomEventData( editingView, {
+ // Clicking a widget.
+ target: editingView.domConverter.viewToDom( viewRoot.getChild( 0 ) ),
+ preventDefault: sinon.spy()
+ } );
+
+ viewDocument.fire( eventInfo, domEventDataMock );
+ sinon.assert.notCalled( typeAroundSpy );
+ } );
} );
describe( 'button to type "after" a widget', () => {
@@ -238,7 +269,7 @@ describe( 'WidgetTypeAround', () => {
viewDocument.fire( eventInfo, domEventDataMock );
sinon.assert.calledOnce( typeAroundSpy );
- sinon.assert.calledWithExactly( typeAroundSpy, viewRoot.getChild( 0 ), 'after' );
+ sinon.assert.calledWithExactly( typeAroundSpy, modelRoot.getChild( 0 ), 'after' );
sinon.assert.calledOnce( preventDefaultSpy );
sinon.assert.calledOnce( stopSpy );
} );
@@ -253,75 +284,1038 @@ describe( 'WidgetTypeAround', () => {
} );
} );
- describe( 'detection and CSS classes of widgets needing the typing around support', () => {
- it( 'should detect widgets that are a first child of the parent container', () => {
- setModelData( editor.model, '
foo' );
+ describe( 'typing around view widgets using keyboard', () => {
+ let model, modelSelection, eventInfoStub, domEventDataStub;
- const viewWidget = viewRoot.getChild( 0 );
+ beforeEach( () => {
+ model = editor.model;
+ modelSelection = model.document.selection;
+ } );
+
+ describe( '"fake caret" activation', () => {
+ it( 'should activate before when the collapsed selection is before a widget and the navigation is forward', () => {
+ setModelData( editor.model, '
foo[]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ const viewWidget = viewRoot.getChild( 1 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should activate after when the collapsed selection is after a widget and the navigation is backward', () => {
+ setModelData( editor.model, '
[]foo' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]
foo' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.true;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should activate after when the widget is selected and the navigation is forward', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.true;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should activate before when the widget is selected and the navigation is backward', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should activate if an arrow key is pressed along with Shift', () => {
+ setModelData( editor.model, '
foo[]' );
+
+ fireKeyboardEvent( 'arrowright', { shiftKey: true } );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should not activate when the selection is before the widget but the non-arrow key was pressed', () => {
+ setModelData( editor.model, '
foo[]' );
+
+ fireKeyboardEvent( 'a' );
+ fireMutation( 'a' );
+
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ expect( getModelData( model ) ).to.equal( '
fooa[]' );
+
+ const viewWidget = viewRoot.getChild( 1 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.notCalled( eventInfoStub.stop );
+ sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should not activate when the selection is not before the widget and navigating forward', () => {
+ setModelData( editor.model, '
fo[]o' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 1 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.notCalled( eventInfoStub.stop );
+ sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should not activate when the selection is not after the widget and navigating backward', () => {
+ setModelData( editor.model, '
f[]oo' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.notCalled( eventInfoStub.stop );
+ sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should not activate when the non-collapsed selection is before the widget and navigating forward', () => {
+ setModelData( editor.model, '
fo[o]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 1 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.notCalled( eventInfoStub.stop );
+ sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should not activate when the non-collapsed selection is after the widget and navigating backward', () => {
+ setModelData( editor.model, '
[f]oo' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.notCalled( eventInfoStub.stop );
+ sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault );
+ } );
- assertIsTypeAroundBefore( viewWidget, true );
- assertIsTypeAroundAfter( viewWidget, false );
+ it( 'should not activate selection downcast when a non–type-around-friendly widget is selected', () => {
+ setModelData( editor.model, '
foo[]' );
+
+ model.change( writer => {
+ // Simply trigger the selection downcast.
+ writer.setSelectionAttribute( 'foo', 'bar' );
+ } );
+
+ const viewWidget = viewRoot.getChild( 0 ).getChild( 1 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.notCalled( eventInfoStub.stop );
+ sinon.assert.notCalled( domEventDataStub.domEvent.preventDefault );
+ } );
} );
- it( 'should detect widgets that are a last child of the parent container', () => {
- setModelData( editor.model, '
foo' );
+ describe( '"fake caret" deactivation', () => {
+ it( 'should deactivate when the widget is selected and the navigation is backward to a valid position', () => {
+ setModelData( editor.model, '
foo[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 1 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should deactivate when the widget is selected and the navigation is forward to a valid position', () => {
+ setModelData( editor.model, '[
]
foo' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]
foo' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '
[]foo' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should deactivate if an arrow key is pressed along with Shift', () => {
+ setModelData( editor.model, '
foo[
]' );
+
+ fireKeyboardEvent( 'arrowleft', { shiftKey: true } );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ fireKeyboardEvent( 'arrowleft', { shiftKey: true } );
+
+ expect( getModelData( model ) ).to.equal( '
foo[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should not deactivate when the widget is selected and the navigation is backward but there is nowhere to go', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should not deactivate when the widget is selected and the navigation is forward but there is nowhere to go', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.true;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should deactivate when the widget is selected and the navigation is against the fake caret (backward)', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should deactivate when the widget is selected and the navigation is against the fake caret (forward)', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ const viewWidget = viewRoot.getChild( 0 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+ } );
+
+ it( 'should activate and deactivate the "fake caret" using all 4 arrow keys', () => {
+ setModelData( editor.model, '
foo[]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireKeyboardEvent( 'arrowdown' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ fireKeyboardEvent( 'arrowup' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should quit the "fake caret" mode when the editor loses focus', () => {
+ editor.ui.focusTracker.isFocused = true;
+
+ setModelData( editor.model, '
foo[]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ editor.ui.focusTracker.isFocused = false;
const viewWidget = viewRoot.getChild( 1 );
- assertIsTypeAroundBefore( viewWidget, false );
- assertIsTypeAroundAfter( viewWidget, true );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
} );
- it( 'should not detect widgets that are surrounded by sibling which allow the selection', () => {
- setModelData( editor.model, '
foobar' );
+ it( 'should quit the "fake caret" mode when the user changed the selection', () => {
+ setModelData( editor.model, '
foo[]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ model.change( writer => {
+ writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' );
+ } );
const viewWidget = viewRoot.getChild( 1 );
- assertIsTypeAroundBefore( viewWidget, false );
- assertIsTypeAroundAfter( viewWidget, false );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.false;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
} );
- it( 'should detect widgets that have another block widget as a next sibling', () => {
- setModelData( editor.model, '
' );
+ it( 'should not quit the "fake caret" mode when the selection changed as a result of an indirect change', () => {
+ setModelData( editor.model, '
foo[]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ // This could happen in collaboration.
+ model.document.selection.fire( 'change:range', {
+ directChange: false
+ } );
- const firstViewWidget = viewRoot.getChild( 0 );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
- assertIsTypeAroundBefore( firstViewWidget, true );
- assertIsTypeAroundAfter( firstViewWidget, true );
+ const viewWidget = viewRoot.getChild( 1 );
+
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_before' ) ).to.be.true;
+ expect( viewWidget.hasClass( 'ck-widget_type-around_show-fake-caret_after' ) ).to.be.false;
} );
- it( 'should detect widgets that have another block widget as a previous sibling', () => {
- setModelData( editor.model, '
' );
+ describe( 'inserting a new paragraph', () => {
+ describe( 'on Enter key press when the "fake caret" is activated', () => {
+ it( 'should insert a paragraph before a widget if the caret was "before" it', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireKeyboardEvent( 'enter' );
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ } );
+
+ it( 'should insert a paragraph after a widget if the caret was "after" it', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireKeyboardEvent( 'enter' );
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ } );
+
+ it( 'should integrate with the undo feature', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+ fireKeyboardEvent( 'enter' );
+
+ expect( getModelData( model ) ).to.equal( '
[]' );
+
+ editor.execute( 'undo' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+ } );
+
+ describe( 'on Enter key press when the widget is selected (no "fake caret", though)', () => {
+ it( 'should insert a new paragraph after the widget if Enter was pressed', () => {
+ setModelData( editor.model, '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ fireKeyboardEvent( 'enter' );
+
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should insert a new paragraph before the widget if Shift+Enter was pressed', () => {
+ setModelData( editor.model, '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ fireKeyboardEvent( 'enter', { shiftKey: true } );
+
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should insert a new paragraph only if an entire widget is selected (selected nested editable content)', () => {
+ setModelData( editor.model, '
[foo] bar' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ fireKeyboardEvent( 'enter' );
+
+ expect( getModelData( model ) ).to.equal( '
[] bar' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should insert a new paragraph only if an entire widget is selected (selected widget siblings)', () => {
+ setModelData( editor.model, '
f[ooo]o' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ fireKeyboardEvent( 'enter' );
+
+ expect( getModelData( model ) ).to.equal( '
f[]o' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should split ancestors to find a place that allows a widget', () => {
+ model.schema.register( 'allowP', {
+ inheritAllFrom: '$block'
+ } );
+ model.schema.register( 'disallowP', {
+ inheritAllFrom: '$block',
+ allowIn: [ 'allowP' ]
+ } );
+ model.schema.extend( 'blockWidget', {
+ allowIn: [ 'allowP', 'disallowP' ]
+ } );
+ model.schema.extend( 'paragraph', {
+ allowIn: [ 'allowP' ]
+ } );
+
+ editor.conversion.for( 'downcast' ).elementToElement( { model: 'allowP', view: 'allowP' } );
+ editor.conversion.for( 'downcast' ).elementToElement( { model: 'disallowP', view: 'disallowP' } );
+
+ setModelData( model,
+ '
' +
+ '[]' +
+ ''
+ );
+
+ fireKeyboardEvent( 'enter' );
+
+ expect( getModelData( model ) ).to.equal(
+ '
' +
+ '' +
+ '[]' +
+ '' +
+ ''
+ );
+ } );
+
+ it( 'should integrate with the undo feature', () => {
+ setModelData( editor.model, '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ fireKeyboardEvent( 'enter' );
+
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ editor.execute( 'undo' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should do nothing if a non-type-around-friendly content is selected', () => {
+ setModelData( editor.model, '
foo[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ fireKeyboardEvent( 'enter' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+ } );
+
+ describe( 'on keydown of a "typing" character when the "fake caret" is activated ', () => {
+ it( 'should insert a character inside a new paragraph before a widget if the caret was "before" it', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
- const lastViewWidget = viewRoot.getChild( 1 );
+ fireKeyboardEvent( 'a' );
+ fireMutation( 'a' );
+
+ expect( getModelData( model ) ).to.equal( '
a[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should insert a character inside a new paragraph after a widget if the caret was "after" it', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireKeyboardEvent( 'a' );
+ fireMutation( 'a' );
+
+ expect( getModelData( model ) ).to.equal( '
a[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+
+ it( 'should do nothing if a "safe" keystroke was pressed', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireKeyboardEvent( 'esc' );
+ fireKeyboardEvent( 'tab' );
+ fireKeyboardEvent( 'd', { ctrlKey: true } );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+ } );
- assertIsTypeAroundBefore( lastViewWidget, true );
- assertIsTypeAroundAfter( lastViewWidget, true );
+ it( 'should integrate with the undo feature', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+ fireKeyboardEvent( 'a' );
+ fireMutation( 'a' );
+
+ expect( getModelData( model ) ).to.equal( '
a[]' );
+
+ editor.execute( 'undo' );
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ editor.execute( 'undo' );
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ } );
+ } );
} );
- it( 'should not detect inline widgets even if they fall in previous categories', () => {
- setModelData( editor.model,
- '
'
- );
+ describe( 'delete integration', () => {
+ let eventInfoStub, domEventDataStub;
+
+ describe( 'backward delete', () => {
+ it( 'should delete content before a widget if the "fake caret" is also before the widget', () => {
+ setModelData( editor.model, '
foo[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal( '
fo[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
- const firstViewWidget = viewRoot.getChild( 0 ).getChild( 0 );
- const lastViewWidget = viewRoot.getChild( 0 ).getChild( 1 );
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete an empty paragraph before a widget if the "fake caret" is also before the widget', () => {
+ setModelData( editor.model, '
[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '
[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete an empty document tree branch before a widget if the "fake caret" is also before the widget', () => {
+ setModelData( editor.model, '
[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
- assertIsTypeAroundBefore( firstViewWidget, false );
- assertIsTypeAroundAfter( firstViewWidget, false );
+ expect( getModelData( model ) ).to.equal(
+ '
' +
+ '' +
+ '
' +
+ '[
]'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
- assertIsTypeAroundBefore( lastViewWidget, false );
- assertIsTypeAroundAfter( lastViewWidget, false );
+ it( 'should delete an empty document tree sub-branch before a widget if the "fake caret" is also before the widget', () => {
+ let operationType;
+
+ setModelData( editor.model,
+ '
' +
+ 'foo' +
+ '' +
+ '
' +
+ '[
]'
+ );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal(
+ '
' +
+ 'foo' +
+ '' +
+ '
' +
+ '[
]'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ // Assert that the paragraph is merged rather than deleted because
+ // it is safer for collaboration.
+ model.on( 'applyOperation', ( evt, [ operation ] ) => {
+ operationType = operation.type;
+ } );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal(
+ '
' +
+ 'foo[]' +
+ '
' +
+ '
'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ expect( operationType ).to.equal( 'merge' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should do nothing if the "fake caret" is before the widget but there is nothing to delete there', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete a widget if the "fake caret" is after the widget (no content after the widget)', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete a widget if the "fake caret" is after the widget (some content after the widget)', () => {
+ setModelData( editor.model, '[
]
foo' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]
foo' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal( '
[]foo' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete a sibling widget', () => {
+ setModelData( editor.model, '
foo[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal(
+ '
foo' +
+ '[
]'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireDeleteEvent();
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+ } );
+
+ describe( 'forward delete', () => {
+ it( 'should delete content after a widget if the "fake caret" is also after the widget', () => {
+ setModelData( editor.model, '[
]
foo' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]
foo' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal( '
[]oo' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete an empty paragraph after a widget if the "fake caret" is also after the widget', () => {
+ setModelData( editor.model, '[
]
' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]
' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete an empty document tree branch after a widget if the "fake caret" is also after the widget', () => {
+ setModelData( editor.model, '[
]
' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal(
+ '[
]' +
+ '
'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete an empty document tree sub-branch after a widget if the "fake caret" is also after the widget', () => {
+ let operationType;
+
+ setModelData( editor.model,
+ '[
]' +
+ '
' +
+ '' +
+ 'foo' +
+ '
'
+ );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal(
+ '[
]' +
+ '
' +
+ '' +
+ 'foo' +
+ '
'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ // Assert that the paragraph is merged rather than deleted because
+ // it is safer for collaboration.
+ model.on( 'applyOperation', ( evt, [ operation ] ) => {
+ operationType = operation.type;
+ } );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal(
+ '
' +
+ '
' +
+ '[]foo' +
+ '
'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+ expect( operationType ).to.equal( 'merge' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should do nothing if the "fake caret" is after the widget but there is nothing to delete there', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete a widget if the "fake caret" is before the widget (no content before the widget)', () => {
+ setModelData( editor.model, '[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete a widget if the "fake caret" is before the widget (some content before the widget)', () => {
+ setModelData( editor.model, '
foo[
]' );
+
+ fireKeyboardEvent( 'arrowleft' );
+
+ expect( getModelData( model ) ).to.equal( '
foo[
]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'before' );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal( '
foo[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+
+ it( 'should delete a sibling widget', () => {
+ setModelData( editor.model, '[
]
foo' );
+
+ fireKeyboardEvent( 'arrowright' );
+
+ expect( getModelData( model ) ).to.equal(
+ '[
]' +
+ '
foo'
+ );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.equal( 'after' );
+
+ fireDeleteEvent( true );
+ expect( getModelData( model ) ).to.equal( '
[]' );
+ expect( modelSelection.getAttribute( 'widget-type-around' ) ).to.be.undefined;
+
+ sinon.assert.calledOnce( eventInfoStub.stop );
+ sinon.assert.calledOnce( domEventDataStub.domEvent.preventDefault );
+ } );
+ } );
+
+ function fireDeleteEvent( isForward = false ) {
+ eventInfoStub = new EventInfo( viewDocument, 'delete' );
+ sinon.spy( eventInfoStub, 'stop' );
+
+ const data = {
+ direction: isForward ? 'forward' : 'backward',
+ unit: 'character'
+ };
+
+ domEventDataStub = new DomEventData( viewDocument, getDomEvent(), data );
+
+ viewDocument.fire( eventInfoStub, domEventDataStub );
+ }
} );
- } );
- function assertIsTypeAroundBefore( viewWidget, expected ) {
- expect( viewWidget.hasClass( 'ck-widget_can-type-around_before' ) ).to.equal( expected );
- }
+ function getDomEvent() {
+ return {
+ preventDefault: sinon.spy(),
+ stopPropagation: sinon.spy()
+ };
+ }
- function assertIsTypeAroundAfter( viewWidget, expected ) {
- expect( viewWidget.hasClass( 'ck-widget_can-type-around_after' ) ).to.equal( expected );
- }
+ function fireKeyboardEvent( key, modifiers ) {
+ eventInfoStub = new EventInfo( viewDocument, 'keydown' );
+
+ sinon.spy( eventInfoStub, 'stop' );
+
+ const data = {
+ document: viewDocument,
+ domTarget: editingView.getDomRoot(),
+ keyCode: getCode( key )
+ };
+
+ Object.assign( data, modifiers );
+
+ domEventDataStub = new DomEventData( viewDocument, getDomEvent(), data );
+
+ viewDocument.fire( eventInfoStub, domEventDataStub );
+ }
+
+ function fireMutation( text ) {
+ const placeOfMutation = viewDocument.selection.getFirstRange().start;
+
+ viewDocument.fire( 'mutations', [
+ {
+ type: 'children',
+ oldChildren: [],
+ newChildren: [ new ViewText( viewDocument, text ) ],
+ node: placeOfMutation
+ }
+ ] );
+ }
+ } );
function blockWidgetPlugin( editor ) {
editor.model.schema.register( 'blockWidget', {
@@ -329,6 +1323,15 @@ describe( 'WidgetTypeAround', () => {
isObject: true
} );
+ editor.model.schema.register( 'nested', {
+ allowIn: 'blockWidget',
+ isLimit: true
+ } );
+
+ editor.model.schema.extend( '$text', {
+ allowIn: [ 'nested' ]
+ } );
+
editor.conversion.for( 'downcast' )
.elementToElement( {
model: 'blockWidget',
@@ -342,6 +1345,10 @@ describe( 'WidgetTypeAround', () => {
label: 'block widget'
} );
}
+ } )
+ .elementToElement( {
+ model: 'nested',
+ view: ( modelItem, viewWriter ) => viewWriter.createEditableElement( 'nested', { contenteditable: true } )
} );
}
diff --git a/packages/ckeditor5-widget/theme/widgettypearound.css b/packages/ckeditor5-widget/theme/widgettypearound.css
index 08eca590299..34d4f0f709d 100644
--- a/packages/ckeditor5-widget/theme/widgettypearound.css
+++ b/packages/ckeditor5-widget/theme/widgettypearound.css
@@ -37,17 +37,6 @@
}
}
- /*
- * Hide the type around buttons depending on which directions the widget supports.
- */
- &:not(.ck-widget_can-type-around_before) > .ck-widget__type-around > .ck-widget__type-around__button_before {
- display: none;
- }
-
- &:not(.ck-widget_can-type-around_after) > .ck-widget__type-around > .ck-widget__type-around__button_after {
- display: none;
- }
-
/*
* Styles for the buttons when:
* - the widget is selected,
@@ -64,6 +53,42 @@
z-index: calc(var(--ck-z-default) + 1);
}
}
+
+ /*
+ * Styles for the horizontal "fake caret" which is displayed when the user navigates using the keyboard.
+ */
+ & > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+ display: none;
+ position: absolute;
+ left: 0;
+ right: 0;
+ }
+
+ /*
+ * When the widget is hovered the "fake caret" would normally be narrower than the
+ * extra outline displayed around the widget. Let's extend the "fake caret" to match
+ * the full width of the widget.
+ */
+ &:hover > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+ left: calc( -1 * var(--ck-widget-outline-thickness) );
+ right: calc( -1 * var(--ck-widget-outline-thickness) );
+ }
+
+ /*
+ * Styles for the horizontal "fake caret" when it should be displayed before the widget (backward keyboard navigation).
+ */
+ &.ck-widget_type-around_show-fake-caret_before > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+ top: calc( -1 * var(--ck-widget-outline-thickness) - 1px );
+ display: block;
+ }
+
+ /*
+ * Styles for the horizontal "fake caret" when it should be displayed after the widget (forward keyboard navigation).
+ */
+ &.ck-widget_type-around_show-fake-caret_after > .ck-widget__type-around > .ck-widget__type-around__fake-caret {
+ bottom: calc( -1 * var(--ck-widget-outline-thickness) - 1px );
+ display: block;
+ }
}
/*