diff --git a/src/image.js b/src/image.js index 96da6626..147dad33 100644 --- a/src/image.js +++ b/src/image.js @@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ImageEngine from './image/imageengine'; import Widget from '@ckeditor/ckeditor5-widget/src/widget'; import ImageTextAlternative from './imagetextalternative'; -import { isImageWidget } from './image/utils'; +import { isImageWidgetSelected } from './image/utils'; import '../theme/theme.scss'; @@ -49,9 +49,7 @@ export default class Image extends Plugin { // https://github.com/ckeditor/ckeditor5-image/issues/110 if ( contextualToolbar ) { this.listenTo( contextualToolbar, 'show', evt => { - const selectedElement = editor.editing.view.selection.getSelectedElement(); - - if ( selectedElement && isImageWidget( selectedElement ) ) { + if ( isImageWidgetSelected( editor.editing.view.selection ) ) { evt.stop(); } }, { priority: 'high' } ); diff --git a/src/image/ui/utils.js b/src/image/ui/utils.js index 55649fbf..65edf118 100644 --- a/src/image/ui/utils.js +++ b/src/image/ui/utils.js @@ -8,7 +8,7 @@ */ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview'; -import { isImageWidget } from '../utils'; +import { isImageWidgetSelected } from '../utils'; /** * A helper utility which positions the @@ -18,11 +18,9 @@ import { isImageWidget } from '../utils'; * @param {module:core/editor/editor~Editor} editor The editor instance. */ export function repositionContextualBalloon( editor ) { - const editingView = editor.editing.view; const balloon = editor.plugins.get( 'ContextualBalloon' ); - const selectedElement = editingView.selection.getSelectedElement(); - if ( selectedElement && isImageWidget( selectedElement ) ) { + if ( isImageWidgetSelected( editor.editing.view.selection ) ) { const position = getBalloonPositionData( editor ); balloon.updatePosition( position ); diff --git a/src/image/utils.js b/src/image/utils.js index 27903acf..e6cf8e78 100644 --- a/src/image/utils.js +++ b/src/image/utils.js @@ -44,6 +44,18 @@ export function isImageWidget( viewElement ) { return !!viewElement.getCustomProperty( imageSymbol ) && isWidget( viewElement ); } +/** + * Checks if an image widget is the only selected element. + * + * @param {module:engine/view/selection~Selection} viewSelection + * @returns {Boolean} + */ +export function isImageWidgetSelected( viewSelection ) { + const viewElement = viewSelection.getSelectedElement(); + + return !!( viewElement && isImageWidget( viewElement ) ); +} + /** * Checks if the provided model element is an instance of {@link module:engine/model/element~Element Element} and its name * is `image`. diff --git a/src/imagetextalternative.js b/src/imagetextalternative.js index 56f94b13..ee89ac7f 100644 --- a/src/imagetextalternative.js +++ b/src/imagetextalternative.js @@ -15,6 +15,7 @@ import TextAlternativeFormView from './imagetextalternative/ui/textalternativefo import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; import textAlternativeIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; import { repositionContextualBalloon, getBalloonPositionData } from './image/ui/utils'; +import { isImageWidgetSelected } from './image/utils'; import '../theme/imagetextalternative/theme.scss'; @@ -119,9 +120,11 @@ export default class ImageTextAlternative extends Plugin { cancel(); } ); - // Reposition the balloon upon #render. + // Reposition the balloon or hide the form if an image widget is no longer selected. this.listenTo( editingView, 'render', () => { - if ( this._isVisible ) { + if ( !isImageWidgetSelected( editingView.selection ) ) { + this._hideForm( true ); + } else if ( this._isVisible ) { repositionContextualBalloon( editor ); } }, { priority: 'low' } ); @@ -176,7 +179,7 @@ export default class ImageTextAlternative extends Plugin { /** * Removes the {@link #_form} from the {@link #_balloon}. * - * @param {Boolean} focusEditable Controls whether the editing view is focused afterwards. + * @param {Boolean} [focusEditable=false] Controls whether the editing view is focused afterwards. * @private */ _hideForm( focusEditable ) { diff --git a/src/imagetoolbar.js b/src/imagetoolbar.js index 9e72af3c..98b8062e 100644 --- a/src/imagetoolbar.js +++ b/src/imagetoolbar.js @@ -11,7 +11,7 @@ import Template from '@ckeditor/ckeditor5-ui/src/template'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; -import { isImageWidget } from './image/utils'; +import { isImageWidgetSelected } from './image/utils'; import { repositionContextualBalloon, getBalloonPositionData } from './image/ui/utils'; const balloonClassName = 'ck-toolbar-container ck-editor-toolbar-container'; @@ -103,10 +103,7 @@ export default class ImageToolbar extends Plugin { if ( !editor.ui.focusTracker.isFocused ) { this._hideToolbar(); } else { - const editingView = editor.editing.view; - const selectedElement = editingView.selection.getSelectedElement(); - - if ( selectedElement && isImageWidget( selectedElement ) ) { + if ( isImageWidgetSelected( editor.editing.view.selection ) ) { this._showToolbar(); } else { this._hideToolbar(); diff --git a/tests/image/utils.js b/tests/image/utils.js index e2c4f4ef..74fe1b24 100644 --- a/tests/image/utils.js +++ b/tests/image/utils.js @@ -4,8 +4,11 @@ */ import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element'; +import ViewSelection from '@ckeditor/ckeditor5-engine/src/view/selection'; +import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; +import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range'; import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element'; -import { toImageWidget, isImageWidget, isImage } from '../../src/image/utils'; +import { toImageWidget, isImageWidget, isImageWidgetSelected, isImage } from '../../src/image/utils'; import { isWidget, getLabel } from '@ckeditor/ckeditor5-widget/src/utils'; describe( 'image widget utils', () => { @@ -49,6 +52,40 @@ describe( 'image widget utils', () => { } ); } ); + describe( 'isImageWidgetSelected()', () => { + let frag; + + it( 'should return true when image widget is the only element in the selection', () => { + // We need to create a container for the element to be able to create a Range on this element. + frag = new ViewDocumentFragment( [ element ] ); + + const selection = new ViewSelection( [ ViewRange.createOn( element ) ] ); + + expect( isImageWidgetSelected( selection ) ).to.be.true; + } ); + + it( 'should return false when non-widgetized elements is the only element in the selection', () => { + const notWidgetizedElement = new ViewElement( 'p' ); + + // We need to create a container for the element to be able to create a Range on this element. + frag = new ViewDocumentFragment( [ notWidgetizedElement ] ); + + const selection = new ViewSelection( [ ViewRange.createOn( notWidgetizedElement ) ] ); + + expect( isImageWidgetSelected( selection ) ).to.be.false; + } ); + + it( 'should return false when widget element is not the only element in the selection', () => { + const notWidgetizedElement = new ViewElement( 'p' ); + + frag = new ViewDocumentFragment( [ element, notWidgetizedElement ] ); + + const selection = new ViewSelection( [ ViewRange.createIn( frag ) ] ); + + expect( isImageWidgetSelected( selection ) ).to.be.false; + } ); + } ); + describe( 'isImage', () => { it( 'should return true for image element', () => { const image = new ModelElement( 'image' ); diff --git a/tests/imagetextalternative.js b/tests/imagetextalternative.js index 034be927..b1f11409 100644 --- a/tests/imagetextalternative.js +++ b/tests/imagetextalternative.js @@ -168,6 +168,22 @@ describe( 'ImageTextAlternative', () => { editingView.fire( 'render' ); sinon.assert.calledOnce( spy ); } ); + + it( 'should hide the form and focus editable when image widget has been removed by external change', () => { + setData( doc, '[]' ); + button.fire( 'execute' ); + + const remveSpy = sinon.spy( balloon, 'remove' ); + const focusSpy = sinon.spy( editor.editing.view, 'focus' ); + + // EnqueueChanges automatically fires #render event. + doc.enqueueChanges( () => { + doc.batch( 'transparent' ).remove( doc.selection.getFirstRange() ); + } ); + + sinon.assert.calledWithExactly( remveSpy, form ); + sinon.assert.calledOnce( focusSpy ); + } ); } ); describe( 'integration with the editor focus', () => { diff --git a/tests/manual/tickets/106/1.js b/tests/manual/tickets/106/1.js index 1e19ab08..91196389 100644 --- a/tests/manual/tickets/106/1.js +++ b/tests/manual/tickets/106/1.js @@ -108,9 +108,14 @@ function startExternalDelete( editor ) { const document = editor.document; const bath = document.batch( 'transparent' ); - wait( 3000 ).then( () => { + function removeSecondBlock() { document.enqueueChanges( () => { bath.remove( Range.createFromPositionAndShift( new Position( document.getRoot(), [ 1 ] ), 1 ) ); } ); - } ); + } + + wait( 3000 ) + .then( () => removeSecondBlock() ) + .then( () => wait( 3000 ) ) + .then( () => removeSecondBlock() ); } diff --git a/tests/manual/tickets/106/1.md b/tests/manual/tickets/106/1.md index 55f73c06..86a0e7e6 100644 --- a/tests/manual/tickets/106/1.md +++ b/tests/manual/tickets/106/1.md @@ -11,3 +11,4 @@ 1. Click **Start external changes** then quickly select the image in the content, then from the toolbar open the **text alternative** editing balloon. 2. Observe if the balloon remains attached to the image as the content is deleted. +3. Wait a bit more and observe if the balloon is removed together with an image.