diff --git a/src/widget.js b/src/widget.js index bdaa05e9..311b83ac 100644 --- a/src/widget.js +++ b/src/widget.js @@ -44,28 +44,38 @@ export default class Widget extends Plugin { init() { const viewDocument = this.editor.editing.view; - let previouslySelected; + /** + * Holds previously selected widgets. + * + * @private + * @type {Set.} + */ + this._previouslySelected = new Set(); // Model to view selection converter. // Converts selection placed over widget element to fake selection this.editor.editing.modelToView.on( 'selection', ( evt, data, consumable, conversionApi ) => { - // Remove selected class from previously selected widget. - if ( previouslySelected && previouslySelected.hasClass( WIDGET_SELECTED_CLASS_NAME ) ) { - previouslySelected.removeClass( WIDGET_SELECTED_CLASS_NAME ); - } + // Remove selected class from previously selected widgets. + this._clearPreviouslySelectedWidgets(); const viewSelection = conversionApi.viewSelection; - - // Check if widget was clicked or some sub-element. const selectedElement = viewSelection.getSelectedElement(); - if ( !selectedElement || !isWidget( selectedElement ) ) { - return; - } + for ( const range of viewSelection.getRanges() ) { + for ( const value of range ) { + const node = value.item; - viewSelection.setFake( true, { label: getLabel( selectedElement ) } ); - selectedElement.addClass( WIDGET_SELECTED_CLASS_NAME ); - previouslySelected = selectedElement; + if ( node.is( 'element' ) && isWidget( node ) ) { + node.addClass( WIDGET_SELECTED_CLASS_NAME ); + this._previouslySelected.add( node ); + + // Check if widget is a single element selected. + if ( node == selectedElement ) { + viewSelection.setFake( true, { label: getLabel( selectedElement ) } ); + } + } + } + } }, { priority: 'low' } ); // If mouse down is pressed on widget - create selection over whole widget. @@ -136,7 +146,7 @@ export default class Widget extends Plugin { } else if ( isArrowKeyCode( keyCode ) ) { wasHandled = this._handleArrowKeys( isForward ); } else if ( isSelectAllKeyCode( domEventData ) ) { - wasHandled = this._selectAllNestedEditableContent(); + wasHandled = this._selectAllNestedEditableContent() || this._selectAllContent(); } if ( wasHandled ) { @@ -256,6 +266,36 @@ export default class Widget extends Plugin { return true; } + /** + * Handles CTRL + A when widget is selected. + * + * @private + * @returns {Boolean} Returns true if widget was selected and selecting all was handled by this method. + */ + _selectAllContent() { + const modelDocument = this.editor.document; + const modelSelection = modelDocument.selection; + const editing = this.editor.editing; + const viewDocument = editing.view; + const viewSelection = viewDocument.selection; + + const selectedElement = viewSelection.getSelectedElement(); + + // Only widget is selected. + // https://github.com/ckeditor/ckeditor5-widget/issues/23 + if ( selectedElement && isWidget( selectedElement ) ) { + const widgetParent = editing.mapper.toModelElement( selectedElement.parent ); + + modelDocument.enqueueChanges( () => { + modelSelection.setRanges( [ ModelRange.createIn( widgetParent ) ] ); + } ); + + return true; + } + + return false; + } + /** * Sets {@link module:engine/model/selection~Selection document's selection} over given element. * @@ -293,6 +333,18 @@ export default class Widget extends Plugin { return null; } + + /** + * Removes CSS class from previously selected widgets. + * @private + */ + _clearPreviouslySelectedWidgets() { + for ( const widget of this._previouslySelected ) { + widget.removeClass( WIDGET_SELECTED_CLASS_NAME ); + } + + this._previouslySelected.clear(); + } } // Returns 'true' if provided key code represents one of the arrow keys. diff --git a/tests/widget.js b/tests/widget.js index e06adfe8..de290d79 100644 --- a/tests/widget.js +++ b/tests/widget.js @@ -196,6 +196,19 @@ describe( 'Widget', () => { expect( viewDocument.selection.fakeSelectionLabel ).to.equal( 'element label' ); } ); + it( 'should add selected class when no only a widget is selected', () => { + setModelData( doc, '[foo]' ); + + expect( viewDocument.selection.isFake ).to.be.false; + expect( getViewData( viewDocument ) ).to.equal( + '[' + + '

foo

' + + '
' + + '
' + + ']' + ); + } ); + it( 'fake selection should be empty if widget is not selected', () => { setModelData( doc, 'foofoo bar' ); @@ -1075,9 +1088,18 @@ describe( 'Widget', () => { { keyCode: keyCodes.a, ctrlKey: true }, 'foo[]bar' ); + + test( + 'should selected whole content when widget is selected', + 'foo[]bar', + { keyCode: keyCodes.a, ctrlKey: true }, + '[foobar]', + '[

foo

bar

]' + + ); } ); - function test( name, data, keyCodeOrMock, expected ) { + function test( name, data, keyCodeOrMock, expected, expectedView ) { it( name, () => { const domEventDataMock = ( typeof keyCodeOrMock == 'object' ) ? keyCodeOrMock : { keyCode: keyCodeOrMock @@ -1091,6 +1113,10 @@ describe( 'Widget', () => { ) ); expect( getModelData( doc ) ).to.equal( expected ); + + if ( expectedView ) { + expect( getViewData( viewDocument ) ).to.equal( expectedView ); + } } ); } } );