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 #219 from ckeditor/t/207
Browse files Browse the repository at this point in the history
Feature: Implemented a CSS–styled image upload loader. Closes #207.
  • Loading branch information
oleq authored Jul 10, 2018
2 parents a65c05a + ade5686 commit 997d39b
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 49 deletions.
64 changes: 46 additions & 18 deletions src/imageupload/imageuploadprogress.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import env from '@ckeditor/ckeditor5-utils/src/env';

import '../../theme/imageuploadprogress.css';
import '../../theme/imageuploadicon.css';
import '../../theme/imageuploadloader.css';

/**
* The image upload progress plugin.
Expand Down Expand Up @@ -120,6 +121,9 @@ export default class ImageUploadProgress extends Plugin {
// Symbol added to progress bar UIElement to distinguish it from other elements.
const progressBarSymbol = Symbol( 'progress-bar' );

// Symbol added to placeholder UIElement to distinguish it from other elements.
const placeholderSymbol = Symbol( 'placeholder' );

// Adds ck-appear class to the image figure if one is not already applied.
//
// @param {module:engine/view/containerelement~ContainerElement} viewFigure
Expand All @@ -140,22 +144,23 @@ function _stopAppearEffect( viewFigure, writer ) {

// Shows placeholder together with infinite progress bar on given image figure.
//
// @param {String} Data-uri with a svg placeholder.
// @param {module:engine/view/containerelement~ContainerElement} viewFigure
// @param {module:engine/view/writer~Writer} writer
function _showPlaceholder( placeholder, viewFigure, writer ) {
if ( !viewFigure.hasClass( 'ck-image-upload-placeholder' ) ) {
writer.addClass( 'ck-image-upload-placeholder', viewFigure );
}

if ( !viewFigure.hasClass( 'ck-infinite-progress' ) ) {
writer.addClass( 'ck-infinite-progress', viewFigure );
}

const viewImg = viewFigure.getChild( 0 );

if ( viewImg.getAttribute( 'src' ) !== placeholder ) {
writer.setAttribute( 'src', placeholder, viewImg );
}

if ( !_getUIElement( viewFigure, placeholderSymbol ) ) {
writer.insert( ViewPosition.createAfter( viewImg ), _createPlaceholder( writer ) );
}
}

// Removes placeholder together with infinite progress bar on given image figure.
Expand All @@ -167,9 +172,7 @@ function _hidePlaceholder( viewFigure, writer ) {
writer.removeClass( 'ck-image-upload-placeholder', viewFigure );
}

if ( viewFigure.hasClass( 'ck-infinite-progress' ) ) {
writer.removeClass( 'ck-infinite-progress', viewFigure );
}
_removeUIElement( viewFigure, writer, placeholderSymbol );
}

// Shows progress bar displaying upload progress.
Expand All @@ -180,7 +183,7 @@ function _hidePlaceholder( viewFigure, writer ) {
// @param {module:upload/filerepository~FileLoader} loader
// @param {module:engine/view/view~View} view
function _showProgressBar( viewFigure, writer, loader, view ) {
const progressBar = createProgressBar( writer );
const progressBar = _createProgressBar( writer );
writer.insert( ViewPosition.createAt( viewFigure, 'end' ), progressBar );

// Update progress bar width when uploadedPercent is changed.
Expand All @@ -196,11 +199,7 @@ function _showProgressBar( viewFigure, writer, loader, view ) {
// @param {module:engine/view/containerelement~ContainerElement} viewFigure
// @param {module:engine/view/writer~Writer} writer
function _hideProgressBar( viewFigure, writer ) {
const progressBar = getProgressBar( viewFigure );

if ( progressBar ) {
writer.remove( ViewRange.createOn( progressBar ) );
}
_removeUIElement( viewFigure, writer, progressBarSymbol );
}

// Shows complete icon and hides after a certain amount of time.
Expand All @@ -223,23 +222,52 @@ function _showCompleteIcon( viewFigure, writer, view ) {
// @private
// @param {module:engine/view/writer~Writer} writer
// @returns {module:engine/view/uielement~UIElement}
function createProgressBar( writer ) {
function _createProgressBar( writer ) {
const progressBar = writer.createUIElement( 'div', { class: 'ck-progress-bar' } );

writer.setCustomProperty( progressBarSymbol, true, progressBar );

return progressBar;
}

// Returns progress bar {@link module:engine/view/uielement~UIElement} from image figure element. Returns `undefined` if
// progress bar element is not found.
// Create placeholder element using {@link module:engine/view/uielement~UIElement}.
//
// @private
// @param {module:engine/view/writer~Writer} writer
// @returns {module:engine/view/uielement~UIElement}
function _createPlaceholder( writer ) {
const placeholder = writer.createUIElement( 'div', { class: 'ck-upload-placeholder-loader' } );

writer.setCustomProperty( placeholderSymbol, true, placeholder );

return placeholder;
}

// Returns {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
// Returns `undefined` if element is not found.
//
// @private
// @param {module:engine/view/element~Element} imageFigure
// @param {Symbol} uniqueProperty
// @returns {module:engine/view/uielement~UIElement|undefined}
function getProgressBar( imageFigure ) {
function _getUIElement( imageFigure, uniqueProperty ) {
for ( const child of imageFigure.getChildren() ) {
if ( child.getCustomProperty( progressBarSymbol ) ) {
if ( child.getCustomProperty( uniqueProperty ) ) {
return child;
}
}
}

// Removes {@link module:engine/view/uielement~UIElement} of given unique property from image figure element.
//
// @private
// @param {module:engine/view/element~Element} imageFigure
// @param {module:engine/view/writer~Writer} writer
// @param {Symbol} uniqueProperty
function _removeUIElement( viewFigure, writer, uniqueProperty ) {
const element = _getUIElement( viewFigure, uniqueProperty );

if ( element ) {
writer.remove( ViewRange.createOn( element ) );
}
}
30 changes: 17 additions & 13 deletions tests/imageupload/imageuploadprogress.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe( 'ImageUploadProgress', () => {

// eslint-disable-next-line max-len
const base64Sample = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=';
let editor, model, document, fileRepository, view, nativeReaderMock, loader, adapterMock;
let editor, model, doc, fileRepository, view, nativeReaderMock, loader, adapterMock;

class UploadAdapterPluginMock extends Plugin {
init() {
Expand Down Expand Up @@ -59,7 +59,7 @@ describe( 'ImageUploadProgress', () => {
.then( newEditor => {
editor = newEditor;
model = editor.model;
document = model.document;
doc = model.document;
view = editor.editing.view;

fileRepository = editor.plugins.get( FileRepository );
Expand All @@ -77,8 +77,9 @@ describe( 'ImageUploadProgress', () => {
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

expect( getViewData( view ) ).to.equal(
'[<figure class="ck-appear ck-image-upload-placeholder ck-infinite-progress ck-widget image" contenteditable="false">' +
'[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
`<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
'<div class="ck-upload-placeholder-loader"></div>' +
'</figure>]<p>foo</p>'
);
} );
Expand Down Expand Up @@ -107,7 +108,7 @@ describe( 'ImageUploadProgress', () => {
const loader = fileRepository.createLoader( file );

setModelData( model, '<image></image>' );
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

// Set attributes directly on image to simulate instant "uploading" status.
model.change( writer => {
Expand All @@ -126,7 +127,7 @@ describe( 'ImageUploadProgress', () => {

it( 'should work correctly when there is no "reading" status and go straight to "uploading" - external changes', () => {
setModelData( model, '<image></image>' );
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

// Set attributes directly on image to simulate instant "uploading" status.
model.change( writer => {
Expand All @@ -135,15 +136,16 @@ describe( 'ImageUploadProgress', () => {
} );

expect( getViewData( view ) ).to.equal(
'[<figure class="ck-appear ck-image-upload-placeholder ck-infinite-progress ck-widget image" contenteditable="false">' +
'[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
`<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
'<div class="ck-upload-placeholder-loader"></div>' +
'</figure>]'
);
} );

it( 'should "clear" image when uploadId changes to null', () => {
setModelData( model, '<image></image>' );
const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

// Set attributes directly on image to simulate instant "uploading" status.
model.change( writer => {
Expand All @@ -158,7 +160,7 @@ describe( 'ImageUploadProgress', () => {

expect( getViewData( view ) ).to.equal(
'[<figure class="ck-widget image" contenteditable="false">' +
`<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
`<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
'</figure>]'
);
} );
Expand All @@ -172,8 +174,8 @@ describe( 'ImageUploadProgress', () => {

expect( getViewData( view ) ).to.equal(
'[<figure class="ck-appear ck-widget image" contenteditable="false">' +
`<img src="${ base64Sample }"></img>` +
'<div class="ck-progress-bar" style="width:40%"></div>' +
`<img src="${ base64Sample }"></img>` +
'<div class="ck-progress-bar" style="width:40%"></div>' +
'</figure>]<p>foo</p>'
);

Expand Down Expand Up @@ -223,8 +225,9 @@ describe( 'ImageUploadProgress', () => {
editor.execute( 'imageUpload', { file: createNativeFileMock() } );

expect( getViewData( view ) ).to.equal(
'[<figure class="ck-appear ck-image-upload-placeholder ck-infinite-progress ck-widget image" contenteditable="false">' +
'[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
`<img src="${ base64Sample }"></img>` +
'<div class="ck-upload-placeholder-loader"></div>' +
'</figure>]<p>foo</p>'
);
} );
Expand All @@ -245,15 +248,16 @@ describe( 'ImageUploadProgress', () => {
it( 'should not show progress bar and complete icon if there is no loader with given uploadId', () => {
setModelData( model, '<image uploadId="123" uploadStatus="reading"></image>' );

const image = document.getRoot().getChild( 0 );
const image = doc.getRoot().getChild( 0 );

model.change( writer => {
writer.setAttribute( 'uploadStatus', 'uploading', image );
} );

expect( getViewData( view ) ).to.equal(
'[<figure class="ck-appear ck-image-upload-placeholder ck-infinite-progress ck-widget image" contenteditable="false">' +
'[<figure class="ck-appear ck-image-upload-placeholder ck-widget image" contenteditable="false">' +
`<img src="data:image/svg+xml;utf8,${ imagePlaceholder }"></img>` +
'<div class="ck-upload-placeholder-loader"></div>' +
'</figure>]'
);

Expand Down
2 changes: 1 addition & 1 deletion tests/manual/imageplaceholder.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<div id="container">
<div id="editor">
</div>
22 changes: 15 additions & 7 deletions tests/manual/imageplaceholder.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,27 @@
* For licensing, see LICENSE.md.
*/

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

import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import ImageEditing from '../../src/image/imageediting';
import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
import ImageUploadProgress from '../../src/imageupload/imageuploadprogress';

VirtualTestEditor.create( { plugins: [ ImageEditing, ImageUploadEditing, ImageUploadProgress ] } )
ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ ImageEditing, ImageUploadEditing, ImageUploadProgress ]
} )
.then( editor => {
const imageUploadProgress = editor.plugins.get( ImageUploadProgress );
const img = document.createElement( 'img' );
window.editor = editor;

img.src = imageUploadProgress.placeholder;
document.getElementById( 'container' ).appendChild( img );
editor.model.change( writer => {
writer.appendElement( 'image', {
uploadId: 'fake',
uploadStatus: 'uploading'
}, editor.model.document.getRoot() );
} );
} )
.catch( error => {
console.error( error );
} );

2 changes: 1 addition & 1 deletion tests/manual/imageplaceholder.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
## Image placeholder

Check if image placeholder is visible.
Check if the image placeholder is visible and looks OK after window resize.
2 changes: 1 addition & 1 deletion theme/icons/image_placeholder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions theme/imageuploadloader.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

.ck .ck-upload-placeholder-loader {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
top: 0;
left: 0;

&::before {
content: '';
position: relative;
}
}
8 changes: 0 additions & 8 deletions theme/imageuploadprogress.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@
position: relative;
overflow: hidden;

/* Infinite progress bar on top while the image is read. */
&.ck-infinite-progress::before {
content: "";
position: absolute;
top: 0;
right: 0;
}

/* Upload progress bar. */
& .ck-progress-bar {
position: absolute;
Expand Down

0 comments on commit 997d39b

Please sign in to comment.