Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #215 from ckeditor/t/142
Browse files Browse the repository at this point in the history
Fix: The UI should update once the image is loaded. Closes #142. 

Used the `EditorUI#update` event instead of `View#render` to attach the UI components (see ckeditor/ckeditor5-core#130).
  • Loading branch information
oleq authored Jun 29, 2018
2 parents 1128cb8 + c9eaa09 commit dee20c0
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 41 deletions.
6 changes: 3 additions & 3 deletions src/image/imageloadobserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Observer from '@ckeditor/ckeditor5-engine/src/view/observer/observer';
/**
* Observes all new images added to the {@link module:engine/view/document~Document},
* fires {@link module:engine/view/document~Document#event:imageLoaded} and
* {@link module:engine/view/document~Document#layoutChanged} event every time when the new image
* {@link module:engine/view/document~Document#event:layoutChanged} event every time when the new image
* has been loaded.
*
* **Note:** This event is not fired for images that has been added to the document and rendered as `complete` (already loaded).
Expand Down Expand Up @@ -82,7 +82,7 @@ export default class ImageLoadObserver extends Observer {
}

/**
* Fires {@link module:engine/view/view/document~Document#event:layoutChanged} and
* Fires {@link module:engine/view/document~Document#event:layoutChanged} and
* {@link module:engine/view/document~Document#event:imageLoaded}
* if observer {@link #isEnabled is enabled}.
*
Expand Down Expand Up @@ -110,7 +110,7 @@ export default class ImageLoadObserver extends Observer {
*
* Introduced by {@link module:image/image/imageloadobserver~ImageLoadObserver}.
*
* @see image/image/imageloadobserver~ImageLoadObserver
* @see module:image/image/imageloadobserver~ImageLoadObserver
* @event module:engine/view/document~Document#event:imageLoaded
* @param {module:engine/view/observer/domeventdata~DomEventData} data Event data.
*/
2 changes: 1 addition & 1 deletion src/imagetextalternative/imagetextalternativeui.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default class ImageTextAlternativeUI extends Plugin {
} );

// Reposition the balloon or hide the form if an image widget is no longer selected.
this.listenTo( view, 'render', () => {
this.listenTo( editor.ui, 'update', () => {
if ( !isImageWidgetSelected( viewDocument.selection ) ) {
this._hideForm( true );
} else if ( this._isVisible ) {
Expand Down
26 changes: 10 additions & 16 deletions src/imagetoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ export default class ImageToolbar extends Plugin {
this._toolbar.fillFromConfig( toolbarConfig, editor.ui.componentFactory );

// Show balloon panel each time image widget is selected.
this.listenTo( editor.editing.view, 'render', () => {
this.listenTo( editor.ui, 'update', () => {
this._checkIsVisible();
} );

// There is no render method after focus is back in editor, we need to check if balloon panel should be visible.
// UI#update is not fired after focus is back in editor, we need to check if balloon panel should be visible.
this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
this._checkIsVisible();
}, { priority: 'low' } );
Expand All @@ -111,14 +111,10 @@ export default class ImageToolbar extends Plugin {
_checkIsVisible() {
const editor = this.editor;

if ( !editor.ui.focusTracker.isFocused ) {
if ( !editor.ui.focusTracker.isFocused || !isImageWidgetSelected( editor.editing.view.document.selection ) ) {
this._hideToolbar();
} else {
if ( isImageWidgetSelected( editor.editing.view.document.selection ) ) {
this._showToolbar();
} else {
this._hideToolbar();
}
this._showToolbar();
}
}

Expand All @@ -132,14 +128,12 @@ export default class ImageToolbar extends Plugin {

if ( this._isVisible ) {
repositionContextualBalloon( editor );
} else {
if ( !this._balloon.hasView( this._toolbar ) ) {
this._balloon.add( {
view: this._toolbar,
position: getBalloonPositionData( editor ),
balloonClassName
} );
}
} else if ( !this._balloon.hasView( this._toolbar ) ) {
this._balloon.add( {
view: this._toolbar,
position: getBalloonPositionData( editor ),
balloonClassName
} );
}
}

Expand Down
43 changes: 34 additions & 9 deletions tests/image/imageediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
* For licensing, see LICENSE.md.
*/

/* global document, Event */

import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import ImageEditing from '../../src/image/imageediting';
import ImageLoadObserver from '../../src/image/imageloadobserver';
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
Expand All @@ -12,7 +15,7 @@ import { isImageWidget } from '../../src/image/utils';
import normalizeHtml from '@ckeditor/ckeditor5-utils/tests/_utils/normalizehtml';

describe( 'ImageEditing', () => {
let editor, model, document, view, viewDocument;
let editor, model, doc, view, viewDocument;

beforeEach( () => {
return VirtualTestEditor
Expand All @@ -22,7 +25,7 @@ describe( 'ImageEditing', () => {
.then( newEditor => {
editor = newEditor;
model = editor.model;
document = model.document;
doc = model.document;
view = editor.editing.view;
viewDocument = view.document;
} );
Expand All @@ -48,6 +51,28 @@ describe( 'ImageEditing', () => {
expect( view.getObserver( ImageLoadObserver ) ).to.be.instanceOf( ImageLoadObserver );
} );

// See https://github.com/ckeditor/ckeditor5-image/issues/142.
it( 'should update the ui after image has been loaded in the DOM', () => {
const element = document.createElement( 'div' );
document.body.appendChild( element );

ClassicTestEditor.create( element, {
plugins: [ ImageEditing ]
} ).then( editor => {
editor.data.set( '<figure class="image"><img src="foo.png" alt="bar" /></figure>' );

const spy = sinon.spy();

editor.ui.on( 'update', spy );

element.querySelector( 'img' ).dispatchEvent( new Event( 'load' ) );

sinon.assert.calledOnce( spy );

return editor.destroy().then( () => element.remove() );
} );
} );

describe( 'conversion in data pipeline', () => {
describe( 'model to view', () => {
it( 'should convert', () => {
Expand Down Expand Up @@ -120,7 +145,7 @@ describe( 'ImageEditing', () => {
'srcset=\'{ "foo":"bar" }\'>' +
'</image>' );

const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );
model.change( writer => {
writer.removeAttribute( 'srcset', image );
} );
Expand Down Expand Up @@ -400,7 +425,7 @@ describe( 'ImageEditing', () => {

it( 'should convert attribute change', () => {
setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

model.change( writer => {
writer.setAttribute( 'alt', 'new text', image );
Expand All @@ -413,7 +438,7 @@ describe( 'ImageEditing', () => {

it( 'should convert attribute removal', () => {
setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

model.change( writer => {
writer.removeAttribute( 'alt', image );
Expand All @@ -425,7 +450,7 @@ describe( 'ImageEditing', () => {

it( 'should not convert change if is already consumed', () => {
setModelData( model, '<image src="foo.png" alt="alt text"></image>' );
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

editor.editing.downcastDispatcher.on( 'attribute:alt:image', ( evt, data, conversionApi ) => {
conversionApi.consumable.consume( data.item, 'attribute:alt' );
Expand Down Expand Up @@ -463,7 +488,7 @@ describe( 'ImageEditing', () => {
'srcset=\'{ "foo":"bar" }\'>' +
'</image>' );

const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );
model.change( writer => {
writer.removeAttribute( 'srcset', image );
} );
Expand Down Expand Up @@ -492,7 +517,7 @@ describe( 'ImageEditing', () => {

it( 'should remove sizes and srcsset attribute when srcset attribute is removed from model', () => {
setModelData( model, '<image src="foo.png" srcset=\'{ "data": "small.png 148w, big.png 1024w" }\'></image>' );
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

model.change( writer => {
writer.removeAttribute( 'srcset', image );
Expand All @@ -512,7 +537,7 @@ describe( 'ImageEditing', () => {
'srcset=\'{ "data": "small.png 148w, big.png 1024w", "width": "1024" }\'>' +
'</image>'
);
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

model.change( writer => {
writer.removeAttribute( 'srcset', image );
Expand Down
5 changes: 2 additions & 3 deletions tests/imagetextalternative/imagetextalternativeui.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,14 @@ describe( 'ImageTextAlternative', () => {
expect( balloon.visibleView ).to.equal( lastView );
} );

describe( 'integration with the editor selection (#render event)', () => {
describe( 'integration with the editor selection (ui#update event)', () => {
it( 'should re-position the form', () => {
setData( model, '[<image src=""></image>]' );
button.fire( 'execute' );

const spy = sinon.spy( balloon, 'updatePosition' );

editor.editing.view.fire( 'render' );
editor.ui.fire( 'update' );
sinon.assert.calledOnce( spy );
} );

Expand All @@ -174,7 +174,6 @@ describe( 'ImageTextAlternative', () => {
const removeSpy = sinon.spy( balloon, 'remove' );
const focusSpy = sinon.spy( editor.editing.view, 'focus' );

// EnqueueChange automatically fires #render event.
model.enqueueChange( 'transparent', writer => {
writer.remove( doc.selection.getFirstRange() );
} );
Expand Down
20 changes: 11 additions & 9 deletions tests/imagetoolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import View from '@ckeditor/ckeditor5-ui/src/view';
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';

describe( 'ImageToolbar', () => {
let editor, model, doc, editingView, plugin, toolbar, balloon, editorElement;
let editor, model, doc, plugin, toolbar, balloon, editorElement;

beforeEach( () => {
editorElement = global.document.createElement( 'div' );
Expand All @@ -36,7 +36,6 @@ describe( 'ImageToolbar', () => {
doc = model.document;
plugin = editor.plugins.get( ImageToolbar );
toolbar = plugin._toolbar;
editingView = editor.editing.view;
balloon = editor.plugins.get( 'ContextualBalloon' );
} );
} );
Expand Down Expand Up @@ -112,17 +111,18 @@ describe( 'ImageToolbar', () => {
} );
} );

describe( 'integration with the editor selection (#change event)', () => {
describe( 'integration with the editor selection', () => {
beforeEach( () => {
editor.ui.focusTracker.isFocused = true;
} );

it( 'should show the toolbar on render when the image is selected', () => {
it( 'should show the toolbar on ui#update when the image is selected', () => {
setData( model, '<paragraph>[foo]</paragraph><image src=""></image>' );

expect( balloon.visibleView ).to.be.null;

editingView.change( () => {} );
editor.ui.fire( 'update' );

expect( balloon.visibleView ).to.be.null;

model.change( writer => {
Expand All @@ -136,12 +136,13 @@ describe( 'ImageToolbar', () => {

// Make sure successive change does not throw, e.g. attempting
// to insert the toolbar twice.
editingView.change( () => {} );
editor.ui.fire( 'update' );
expect( balloon.visibleView ).to.equal( toolbar );
} );

it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
setData( model, '[<image src=""></image>]' );

expect( balloon.visibleView ).to.equal( toolbar );

const lastView = new View();
Expand All @@ -156,11 +157,12 @@ describe( 'ImageToolbar', () => {

expect( balloon.visibleView ).to.equal( lastView );

editingView.change( () => {} );
editor.ui.fire( 'update' );

expect( balloon.visibleView ).to.equal( lastView );
} );

it( 'should hide the toolbar on render if the image is de–selected', () => {
it( 'should hide the toolbar on ui#update if the image is de–selected', () => {
setData( model, '<paragraph>foo</paragraph>[<image src=""></image>]' );

expect( balloon.visibleView ).to.equal( toolbar );
Expand All @@ -176,7 +178,7 @@ describe( 'ImageToolbar', () => {

// Make sure successive change does not throw, e.g. attempting
// to remove the toolbar twice.
editingView.change( () => {} );
editor.ui.fire( 'update' );
expect( balloon.visibleView ).to.be.null;
} );
} );
Expand Down
6 changes: 6 additions & 0 deletions tests/manual/tickets/142/1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<p><button class="start">Insert image</button></p>

<div id="editor">
<p>lorem ipsum</p>
<p>foo <a href="foo">bar</a> biz</p>
</div>
43 changes: 43 additions & 0 deletions tests/manual/tickets/142/1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* global document, console, window, setTimeout */

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Link from '@ckeditor/ckeditor5-link/src/link';
import BlockToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/block/blocktoolbar';
import Image from '../../../../src/image';
import ImageCaption from '../../../../src/imagecaption';

ClassicEditor
.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Paragraph, Link, Image, ImageCaption, BlockToolbar ],
toolbar: [],
blockToolbar: [ 'Link' ]
} )
.then( editor => {
window.editor = editor;

const doc = editor.model.document;

document.querySelector( '.start' ).addEventListener( 'click', () => {
let image;

editor.model.change( writer => {
image = writer.createElement( 'image', { src: 'sample-small.jpg' } );
writer.insert( image, doc.getRoot().getChild( 0 ), 'after' );
} );

setTimeout( () => {
editor.ui.view.element.querySelector( 'img' ).src = '../../sample.jpg';
}, 3000 );
} );
} )
.catch( err => {
console.error( err.stack );
} );
4 changes: 4 additions & 0 deletions tests/manual/tickets/142/1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Update the UI after image load [#142](https://github.com/ckeditor/ckeditor5-image/issues/142)

1. Click **Insert image** then quickly select the link in the content (the link editing balloon should show up).
2. Observe if the link balloon and block toolbar remains properly positioned after replacing image source to the biggest one (replacing source is made using DOM API).
Binary file added tests/manual/tickets/142/sample-small.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/manual/tickets/142/sample.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit dee20c0

Please sign in to comment.