mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-26 12:21:36 +03:00
Merge pull request #6651 from kevinansfield/uploader-js-must-die
Replace jQuery-based uploader.js with ember components
This commit is contained in:
commit
a2a825bfe9
@ -1,257 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
import ghostPaths from 'ghost/utils/ghost-paths';
|
|
||||||
|
|
||||||
const {$} = Ember;
|
|
||||||
|
|
||||||
let Ghost = ghostPaths();
|
|
||||||
|
|
||||||
let UploadUi = function ($dropzone, settings) {
|
|
||||||
let $url = '<div class="js-url"><input class="url js-upload-url gh-input" type="url" placeholder="http://"/></div>';
|
|
||||||
let $cancel = '<a class="image-cancel icon-trash js-cancel" title="Delete"><span class="hidden">Delete</span></a>';
|
|
||||||
let $progress = $('<div />', {
|
|
||||||
class: 'js-upload-progress progress progress-success active',
|
|
||||||
role: 'progressbar',
|
|
||||||
'aria-valuemin': '0',
|
|
||||||
'aria-valuemax': '100'
|
|
||||||
}).append($('<div />', {
|
|
||||||
class: 'js-upload-progress-bar bar',
|
|
||||||
style: 'width:0%'
|
|
||||||
}));
|
|
||||||
|
|
||||||
$.extend(this, {
|
|
||||||
complete: (result) => {
|
|
||||||
let showImage = (width, height) => {
|
|
||||||
$dropzone.find('img.js-upload-target').attr({width, height}).css({display: 'block'});
|
|
||||||
$dropzone.find('.fileupload-loading').remove();
|
|
||||||
$dropzone.css({height: 'auto'});
|
|
||||||
$dropzone.delay(250).animate({opacity: 100}, 1000, () => {
|
|
||||||
$('.js-button-accept').prop('disabled', false);
|
|
||||||
this.init();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let animateDropzone = ($img) => {
|
|
||||||
$dropzone.animate({opacity: 0}, 250, () => {
|
|
||||||
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
|
|
||||||
this.removeExtras();
|
|
||||||
$dropzone.animate({height: $img.height()}, 250, () => {
|
|
||||||
showImage($img.width(), $img.height());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let preLoadImage = () => {
|
|
||||||
let $img = $dropzone.find('img.js-upload-target')
|
|
||||||
.attr({src: '', width: 'auto', height: 'auto'});
|
|
||||||
|
|
||||||
$progress.animate({opacity: 0}, 250, () => {
|
|
||||||
$dropzone.find('span.media').after(`<img class="fileupload-loading" src="${Ghost.subdir}/ghost/img/loadingcat.gif" />`);
|
|
||||||
});
|
|
||||||
$img.one('load', () => {
|
|
||||||
$dropzone.trigger('uploadsuccess', [result]);
|
|
||||||
animateDropzone($img);
|
|
||||||
}).attr('src', result);
|
|
||||||
};
|
|
||||||
preLoadImage();
|
|
||||||
},
|
|
||||||
|
|
||||||
bindFileUpload() {
|
|
||||||
$dropzone.find('.js-fileupload').fileupload().fileupload('option', {
|
|
||||||
url: `${Ghost.apiRoot}/uploads/`,
|
|
||||||
add(e, data) {
|
|
||||||
/*jshint unused:false*/
|
|
||||||
$('.js-button-accept').prop('disabled', true);
|
|
||||||
$dropzone.find('.js-fileupload').removeClass('right');
|
|
||||||
$dropzone.find('.js-url').remove();
|
|
||||||
$progress.find('.js-upload-progress-bar').removeClass('fail');
|
|
||||||
$dropzone.trigger('uploadstart', [$dropzone.attr('id')]);
|
|
||||||
$dropzone.find('span.media, div.description, a.image-url, a.image-webcam')
|
|
||||||
.animate({opacity: 0}, 250, () => {
|
|
||||||
$dropzone.find('div.description').hide().css({opacity: 100});
|
|
||||||
if (settings.progressbar) {
|
|
||||||
$dropzone.find('div.js-fail').after($progress);
|
|
||||||
$progress.animate({opacity: 100}, 250);
|
|
||||||
}
|
|
||||||
data.submit();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
dropZone: settings.fileStorage ? $dropzone : null,
|
|
||||||
progressall(e, data) {
|
|
||||||
/*jshint unused:false*/
|
|
||||||
let progress = parseInt(data.loaded / data.total * 100, 10);
|
|
||||||
if (settings.progressbar) {
|
|
||||||
$dropzone.trigger('uploadprogress', [progress, data]);
|
|
||||||
$progress.find('.js-upload-progress-bar').css('width', `${progress}%`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: (e, data) => {
|
|
||||||
/*jshint unused:false*/
|
|
||||||
$('.js-button-accept').prop('disabled', false);
|
|
||||||
$dropzone.trigger('uploadfailure', [data.result]);
|
|
||||||
$dropzone.find('.js-upload-progress-bar').addClass('fail');
|
|
||||||
if (data.jqXHR.status === 413) {
|
|
||||||
$dropzone.find('div.js-fail').text('The image you uploaded was larger than the maximum file size your server allows.');
|
|
||||||
} else if (data.jqXHR.status === 415) {
|
|
||||||
$dropzone.find('div.js-fail').text('The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.');
|
|
||||||
} else {
|
|
||||||
$dropzone.find('div.js-fail').text('Something went wrong :(');
|
|
||||||
}
|
|
||||||
$dropzone.find('div.js-fail, button.js-fail').fadeIn(1500);
|
|
||||||
$dropzone.find('button.js-fail').on('click', () => {
|
|
||||||
$dropzone.css({minHeight: 0});
|
|
||||||
$dropzone.find('div.description').show();
|
|
||||||
this.removeExtras();
|
|
||||||
this.init();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
done: (e, data) => {
|
|
||||||
/*jshint unused:false*/
|
|
||||||
this.complete(data.result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
buildExtras() {
|
|
||||||
if (!$dropzone.find('span.media')[0]) {
|
|
||||||
$dropzone.prepend('<span class="media"><span class="hidden">Image Upload</span></span>');
|
|
||||||
}
|
|
||||||
if (!$dropzone.find('div.description')[0]) {
|
|
||||||
$dropzone.append('<div class="description">Add image</div>');
|
|
||||||
}
|
|
||||||
if (!$dropzone.find('div.js-fail')[0]) {
|
|
||||||
$dropzone.append('<div class="js-fail failed" style="display: none">Something went wrong :(</div>');
|
|
||||||
}
|
|
||||||
if (!$dropzone.find('button.js-fail')[0]) {
|
|
||||||
$dropzone.append('<button class="js-fail btn btn-green" style="display: none">Try Again</button>');
|
|
||||||
}
|
|
||||||
if (!$dropzone.find('a.image-url')[0]) {
|
|
||||||
$dropzone.append('<a class="image-url" title="Add image from URL"><i class="icon-link"><span class="hidden">URL</span></i></a>');
|
|
||||||
}
|
|
||||||
// if (!$dropzone.find('a.image-webcam')[0]) {
|
|
||||||
// $dropzone.append('<a class="image-webcam" title="Add image from webcam"><span class="hidden">Webcam</span></a>');
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
|
|
||||||
removeExtras() {
|
|
||||||
$dropzone.find('span.media, div.js-upload-progress, a.image-url, a.image-upload, a.image-webcam, div.js-fail, button.js-fail, a.js-cancel, button.js-button-accept').remove();
|
|
||||||
},
|
|
||||||
|
|
||||||
initWithDropzone() {
|
|
||||||
// This is the start point if no image exists
|
|
||||||
$dropzone.find('img.js-upload-target').css({display: 'none'});
|
|
||||||
$dropzone.find('div.description').show();
|
|
||||||
$dropzone.removeClass('pre-image-uploader image-uploader-url').addClass('image-uploader');
|
|
||||||
this.removeExtras();
|
|
||||||
this.buildExtras();
|
|
||||||
this.bindFileUpload();
|
|
||||||
if (!settings.fileStorage) {
|
|
||||||
this.initUrl();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$dropzone.find('a.image-url').on('click', () => {
|
|
||||||
this.initUrl();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
initUrl() {
|
|
||||||
this.removeExtras();
|
|
||||||
$dropzone.addClass('image-uploader-url').removeClass('pre-image-uploader');
|
|
||||||
$dropzone.find('.js-fileupload').addClass('right');
|
|
||||||
$dropzone.find('.js-cancel').on('click', () => {
|
|
||||||
$dropzone.find('.js-url').remove();
|
|
||||||
$dropzone.find('.js-fileupload').removeClass('right');
|
|
||||||
$dropzone.trigger('imagecleared');
|
|
||||||
this.removeExtras();
|
|
||||||
this.initWithDropzone();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!$dropzone.find('.js-url')[0]) {
|
|
||||||
$dropzone.find('div.description').before($url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.editor) {
|
|
||||||
$dropzone.find('div.js-url').append('<button class="btn btn-blue js-button-accept gh-input">Save</button>');
|
|
||||||
$dropzone.find('div.description').hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
$dropzone.find('.js-button-accept').on('click', () => {
|
|
||||||
let val = $dropzone.find('.js-upload-url').val();
|
|
||||||
|
|
||||||
$dropzone.find('div.description').hide();
|
|
||||||
$dropzone.find('.js-fileupload').removeClass('right');
|
|
||||||
$dropzone.find('.js-url').remove();
|
|
||||||
if (val === '') {
|
|
||||||
$dropzone.trigger('uploadsuccess', 'http://');
|
|
||||||
this.initWithDropzone();
|
|
||||||
} else {
|
|
||||||
this.complete(val);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only show the toggle icon if there is a dropzone mode to go back to
|
|
||||||
if (settings.fileStorage !== false) {
|
|
||||||
$dropzone.append('<a class="image-upload icon-photos" title="Add image"><span class="hidden">Upload</span></a>');
|
|
||||||
}
|
|
||||||
|
|
||||||
$dropzone.find('a.image-upload').on('click', () => {
|
|
||||||
$dropzone.find('.js-url').remove();
|
|
||||||
$dropzone.find('.js-fileupload').removeClass('right');
|
|
||||||
this.initWithDropzone();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
initWithImage() {
|
|
||||||
// This is the start point if an image already exists
|
|
||||||
this.removeExtras();
|
|
||||||
$dropzone.removeClass('image-uploader image-uploader-url').addClass('pre-image-uploader');
|
|
||||||
$dropzone.find('div.description').hide();
|
|
||||||
$dropzone.find('img.js-upload-target').show();
|
|
||||||
$dropzone.append($cancel);
|
|
||||||
$dropzone.find('.js-cancel').on('click', () => {
|
|
||||||
$dropzone.find('img.js-upload-target').attr({src: ''});
|
|
||||||
$dropzone.find('div.description').show();
|
|
||||||
$dropzone.trigger('imagecleared');
|
|
||||||
|
|
||||||
$dropzone.trigger('uploadsuccess', 'http://');
|
|
||||||
this.initWithDropzone();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
init() {
|
|
||||||
let imageTarget = $dropzone.find('img.js-upload-target');
|
|
||||||
// First check if field image is defined by checking for js-upload-target class
|
|
||||||
if (!imageTarget[0]) {
|
|
||||||
// This ensures there is an image we can hook into to display uploaded image
|
|
||||||
$dropzone.prepend('<img class="js-upload-target" style="display: none" src="" />');
|
|
||||||
}
|
|
||||||
$('.js-button-accept').prop('disabled', false);
|
|
||||||
if (imageTarget.attr('src') === '' || imageTarget.attr('src') === undefined) {
|
|
||||||
this.initWithDropzone();
|
|
||||||
} else {
|
|
||||||
this.initWithImage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
$dropzone.find('.js-url').remove();
|
|
||||||
$dropzone.find('.js-fileupload').removeClass('right');
|
|
||||||
this.removeExtras();
|
|
||||||
this.initWithDropzone();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function (options) {
|
|
||||||
let settings = $.extend({
|
|
||||||
progressbar: true,
|
|
||||||
editor: false,
|
|
||||||
fileStorage: true
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
return this.each(function () {
|
|
||||||
let $dropzone = $(this);
|
|
||||||
let ui = new UploadUi($dropzone, settings);
|
|
||||||
$(this).attr('data-uploaderui', true);
|
|
||||||
this.uploaderUi = ui;
|
|
||||||
ui.init();
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,23 +1,25 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
import uploader from 'ghost/assets/lib/uploader';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
$,
|
$,
|
||||||
Component,
|
Component,
|
||||||
inject: {service},
|
run,
|
||||||
run
|
uuid
|
||||||
} = Ember;
|
} = Ember;
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
config: service(),
|
|
||||||
|
|
||||||
_scrollWrapper: null,
|
_scrollWrapper: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.set('imageUploadComponents', Ember.A([]));
|
||||||
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this._scrollWrapper = this.$().closest('.entry-preview-content');
|
this._scrollWrapper = this.$().closest('.entry-preview-content');
|
||||||
this.adjustScrollPosition(this.get('scrollPosition'));
|
this.adjustScrollPosition(this.get('scrollPosition'));
|
||||||
run.scheduleOnce('afterRender', this, this.dropzoneHandler);
|
run.scheduleOnce('afterRender', this, this.registerImageUploadComponents);
|
||||||
},
|
},
|
||||||
|
|
||||||
didReceiveAttrs(attrs) {
|
didReceiveAttrs(attrs) {
|
||||||
@ -32,7 +34,14 @@ export default Component.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) {
|
if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) {
|
||||||
run.scheduleOnce('afterRender', this, this.dropzoneHandler);
|
// we need to clear the rendered components as we are unable to
|
||||||
|
// retain a reliable reference for the component's position in the
|
||||||
|
// document
|
||||||
|
// TODO: it may be possible to extract the dropzones and use the
|
||||||
|
// image src as a key, re-connecting any that match and
|
||||||
|
// dropping/re-rendering any unknown/no-source instances
|
||||||
|
this.set('imageUploadComponents', Ember.A([]));
|
||||||
|
run.scheduleOnce('afterRender', this, this.registerImageUploadComponents);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -44,22 +53,38 @@ export default Component.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
dropzoneHandler() {
|
registerImageUploadComponents() {
|
||||||
let dropzones = $('.js-drop-zone[data-uploaderui!="true"]');
|
let dropzones = $('.js-drop-zone');
|
||||||
|
|
||||||
if (dropzones.length) {
|
dropzones.each((i, el) => {
|
||||||
uploader.call(dropzones, {
|
let id = uuid();
|
||||||
editor: true,
|
let destinationElementId = `image-uploader-${id}`;
|
||||||
fileStorage: this.get('config.fileStorage')
|
let src = $(el).find('.js-upload-target').attr('src');
|
||||||
|
|
||||||
|
let imageUpload = Ember.Object.create({
|
||||||
|
destinationElementId,
|
||||||
|
id,
|
||||||
|
src,
|
||||||
|
index: i
|
||||||
});
|
});
|
||||||
|
|
||||||
dropzones.on('uploadstart', run.bind(this, 'sendAction', 'uploadStarted'));
|
el.id = destinationElementId;
|
||||||
dropzones.on('uploadfailure', run.bind(this, 'sendAction', 'uploadFinished'));
|
$(el).empty();
|
||||||
dropzones.on('uploadsuccess', run.bind(this, 'sendAction', 'uploadFinished'));
|
$(el).removeClass('image-uploader');
|
||||||
dropzones.on('uploadsuccess', run.bind(this, 'sendAction', 'uploadSuccess'));
|
|
||||||
|
|
||||||
// Set the current height so we can listen
|
run.schedule('afterRender', () => {
|
||||||
this.sendAction('updateHeight', this.$().height());
|
this.get('imageUploadComponents').pushObject(imageUpload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
updateImageSrc(index, url) {
|
||||||
|
this.attrs.updateImageSrc(index, url);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHeight() {
|
||||||
|
this.attrs.updateHeight(this.$().height());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -100,18 +100,18 @@ export default Component.extend(ShortcutsMixin, {
|
|||||||
|
|
||||||
// Match the uploaded file to a line in the editor, and update that line with a path reference
|
// Match the uploaded file to a line in the editor, and update that line with a path reference
|
||||||
// ensuring that everything ends up in the correct place and format.
|
// ensuring that everything ends up in the correct place and format.
|
||||||
handleImgUpload(e, resultSrc) {
|
handleImgUpload(imageIndex, newSrc) {
|
||||||
let editor = this.get('editor');
|
let editor = this.get('editor');
|
||||||
let editorValue = editor.getValue();
|
let editorValue = editor.getValue();
|
||||||
let replacement = imageManager.getSrcRange(editorValue, e.target);
|
let replacement = imageManager.getSrcRange(editorValue, imageIndex);
|
||||||
let cursorPosition;
|
let cursorPosition;
|
||||||
|
|
||||||
if (replacement) {
|
if (replacement) {
|
||||||
cursorPosition = replacement.start + resultSrc.length + 1;
|
cursorPosition = replacement.start + newSrc.length + 1;
|
||||||
if (replacement.needsParens) {
|
if (replacement.needsParens) {
|
||||||
resultSrc = `(${resultSrc})`;
|
newSrc = `(${newSrc})`;
|
||||||
}
|
}
|
||||||
editor.replaceSelection(resultSrc, replacement.start, replacement.end, cursorPosition);
|
editor.replaceSelection(newSrc, replacement.start, replacement.end, cursorPosition);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
39
ghost/admin/app/components/gh-image-uploader-with-preview.js
Normal file
39
ghost/admin/app/components/gh-image-uploader-with-preview.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
actions: {
|
||||||
|
update() {
|
||||||
|
if (typeof this.attrs.update === 'function') {
|
||||||
|
this.attrs.update(...arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInput() {
|
||||||
|
if (typeof this.attrs.onInput === 'function') {
|
||||||
|
this.attrs.onInput(...arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadStarted() {
|
||||||
|
if (typeof this.attrs.uploadStarted === 'function') {
|
||||||
|
this.attrs.uploadStarted(...arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFinished() {
|
||||||
|
if (typeof this.attrs.uploadFinished === 'function') {
|
||||||
|
this.attrs.uploadFinished(...arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formChanged() {
|
||||||
|
if (typeof this.attrs.formChanged === 'function') {
|
||||||
|
this.attrs.formChanged(...arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
217
ghost/admin/app/components/gh-image-uploader.js
Normal file
217
ghost/admin/app/components/gh-image-uploader.js
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
import ghostPaths from 'ghost/utils/ghost-paths';
|
||||||
|
import {RequestEntityTooLargeError, UnsupportedMediaTypeError} from 'ghost/services/ajax';
|
||||||
|
|
||||||
|
const {
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
inject: {service},
|
||||||
|
isBlank,
|
||||||
|
run
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: 'section',
|
||||||
|
classNames: ['gh-image-uploader'],
|
||||||
|
classNameBindings: ['dragClass'],
|
||||||
|
|
||||||
|
image: null,
|
||||||
|
text: 'Upload an image',
|
||||||
|
saveButton: true,
|
||||||
|
|
||||||
|
dragClass: null,
|
||||||
|
failureMessage: null,
|
||||||
|
file: null,
|
||||||
|
formType: 'upload',
|
||||||
|
url: null,
|
||||||
|
uploadPercentage: 0,
|
||||||
|
|
||||||
|
ajax: service(),
|
||||||
|
config: service(),
|
||||||
|
session: service(),
|
||||||
|
|
||||||
|
// TODO: this wouldn't be necessary if the server could accept direct
|
||||||
|
// file uploads
|
||||||
|
formData: computed('file', function () {
|
||||||
|
let file = this.get('file');
|
||||||
|
let formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('uploadimage', file);
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}),
|
||||||
|
|
||||||
|
progressStyle: computed('uploadPercentage', function () {
|
||||||
|
let percentage = this.get('uploadPercentage');
|
||||||
|
let width = '';
|
||||||
|
|
||||||
|
if (percentage > 0) {
|
||||||
|
width = `${percentage}%`;
|
||||||
|
} else {
|
||||||
|
width = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ember.String.htmlSafe(`width: ${width}`);
|
||||||
|
}),
|
||||||
|
|
||||||
|
canShowUploadForm: computed('config.fileStorage', function () {
|
||||||
|
return this.get('config.fileStorage') !== false;
|
||||||
|
}),
|
||||||
|
|
||||||
|
showUploadForm: computed('formType', function () {
|
||||||
|
let canShowUploadForm = this.get('canShowUploadForm');
|
||||||
|
let formType = this.get('formType');
|
||||||
|
|
||||||
|
return formType === 'upload' && canShowUploadForm;
|
||||||
|
}),
|
||||||
|
|
||||||
|
didReceiveAttrs() {
|
||||||
|
let image = this.get('image');
|
||||||
|
this.set('url', image);
|
||||||
|
},
|
||||||
|
|
||||||
|
dragOver(event) {
|
||||||
|
let showUploadForm = this.get('showUploadForm');
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (showUploadForm) {
|
||||||
|
this.set('dragClass', '--drag-over');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dragLeave(event) {
|
||||||
|
let showUploadForm = this.get('showUploadForm');
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (showUploadForm) {
|
||||||
|
this.set('dragClass', null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
drop(event) {
|
||||||
|
let showUploadForm = this.get('showUploadForm');
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.set('dragClass', null);
|
||||||
|
|
||||||
|
if (showUploadForm) {
|
||||||
|
if (event.dataTransfer.files) {
|
||||||
|
this.send('fileSelected', event.dataTransfer.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadStarted() {
|
||||||
|
if (typeof this.attrs.uploadStarted === 'function') {
|
||||||
|
this.attrs.uploadStarted();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadProgress(event) {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
run(() => {
|
||||||
|
let percentage = Math.round((event.loaded / event.total) * 100);
|
||||||
|
this.set('uploadPercentage', percentage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFinished() {
|
||||||
|
if (typeof this.attrs.uploadFinished === 'function') {
|
||||||
|
this.attrs.uploadFinished();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadSuccess(response) {
|
||||||
|
this.set('url', response);
|
||||||
|
this.send('saveUrl');
|
||||||
|
this.send('reset');
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadFailed(error) {
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (error instanceof UnsupportedMediaTypeError) {
|
||||||
|
message = 'The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.';
|
||||||
|
} else if (error instanceof RequestEntityTooLargeError) {
|
||||||
|
message = 'The image you uploaded was larger than the maximum file size your server allows.';
|
||||||
|
} else if (error.errors && !isBlank(error.errors[0].message)) {
|
||||||
|
message = error.errors[0].message;
|
||||||
|
} else {
|
||||||
|
message = 'Something went wrong :(';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('failureMessage', message);
|
||||||
|
},
|
||||||
|
|
||||||
|
generateRequest() {
|
||||||
|
let ajax = this.get('ajax');
|
||||||
|
let formData = this.get('formData');
|
||||||
|
let url = `${ghostPaths().apiRoot}/uploads/`;
|
||||||
|
|
||||||
|
this.uploadStarted();
|
||||||
|
|
||||||
|
ajax.post(url, {
|
||||||
|
data: formData,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
dataType: 'text',
|
||||||
|
xhr: () => {
|
||||||
|
let xhr = new window.XMLHttpRequest();
|
||||||
|
|
||||||
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
|
this.uploadProgress(event);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
return xhr;
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
let url = JSON.parse(response);
|
||||||
|
this.uploadSuccess(url);
|
||||||
|
}).catch((error) => {
|
||||||
|
this.uploadFailed(error);
|
||||||
|
}).finally(() => {
|
||||||
|
this.uploadFinished();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
fileSelected(fileList) {
|
||||||
|
this.set('file', fileList[0]);
|
||||||
|
run.schedule('actions', this, function () {
|
||||||
|
this.generateRequest();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onInput(url) {
|
||||||
|
this.set('url', url);
|
||||||
|
|
||||||
|
if (typeof this.attrs.onInput === 'function') {
|
||||||
|
this.attrs.onInput(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.set('file', null);
|
||||||
|
this.set('uploadPercentage', 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
switchForm(formType) {
|
||||||
|
this.set('formType', formType);
|
||||||
|
|
||||||
|
if (typeof this.attrs.formChanged === 'function') {
|
||||||
|
run.scheduleOnce('afterRender', this, function () {
|
||||||
|
this.attrs.formChanged(formType);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveUrl() {
|
||||||
|
let url = this.get('url');
|
||||||
|
this.attrs.update(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -119,10 +119,6 @@ export default Component.extend({
|
|||||||
this.get('setProperty')('image', '');
|
this.get('setProperty')('image', '');
|
||||||
},
|
},
|
||||||
|
|
||||||
setUploaderReference() {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
|
|
||||||
openMeta() {
|
openMeta() {
|
||||||
this.set('isViewingSubview', true);
|
this.set('isViewingSubview', true);
|
||||||
},
|
},
|
||||||
|
@ -1,88 +0,0 @@
|
|||||||
import Ember from 'ember';
|
|
||||||
import uploader from 'ghost/assets/lib/uploader';
|
|
||||||
|
|
||||||
const {
|
|
||||||
Component,
|
|
||||||
computed,
|
|
||||||
get,
|
|
||||||
inject: {service},
|
|
||||||
isEmpty,
|
|
||||||
run
|
|
||||||
} = Ember;
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
classNames: ['image-uploader', 'js-post-image-upload'],
|
|
||||||
|
|
||||||
config: service(),
|
|
||||||
|
|
||||||
imageSource: computed('image', function () {
|
|
||||||
return this.get('image') || '';
|
|
||||||
}),
|
|
||||||
|
|
||||||
// removes event listeners from the uploader
|
|
||||||
removeListeners() {
|
|
||||||
let $this = this.$();
|
|
||||||
|
|
||||||
$this.off();
|
|
||||||
$this.find('.js-cancel').off();
|
|
||||||
},
|
|
||||||
|
|
||||||
// NOTE: because the uploader is sometimes in the same place in the DOM
|
|
||||||
// between transitions Glimmer will re-use the existing elements including
|
|
||||||
// those that arealready decorated by jQuery. The following works around
|
|
||||||
// situations where the image is changed without a full teardown/rebuild
|
|
||||||
didReceiveAttrs(attrs) {
|
|
||||||
let oldValue = attrs.oldAttrs && get(attrs.oldAttrs, 'image.value');
|
|
||||||
let newValue = attrs.newAttrs && get(attrs.newAttrs, 'image.value');
|
|
||||||
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
// always reset when we receive a blank image
|
|
||||||
// - handles navigating to populated image from blank image
|
|
||||||
if (isEmpty(newValue) && !isEmpty(oldValue)) {
|
|
||||||
this.$()[0].uploaderUi.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-init if we receive a new image
|
|
||||||
// - handles back button navigating from blank image to populated image
|
|
||||||
// - handles navigating between populated images
|
|
||||||
|
|
||||||
if (!isEmpty(newValue) && this.$()) {
|
|
||||||
this.$('.js-upload-target').attr('src', '');
|
|
||||||
this.$()[0].uploaderUi.reset();
|
|
||||||
this.$()[0].uploaderUi.initWithImage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.send('initUploader');
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.removeListeners();
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
initUploader() {
|
|
||||||
let el = this.$();
|
|
||||||
let ref = uploader.call(el, {
|
|
||||||
editor: true,
|
|
||||||
fileStorage: this.get('config.fileStorage')
|
|
||||||
});
|
|
||||||
|
|
||||||
el.on('uploadsuccess', (event, result) => {
|
|
||||||
if (result && result !== '' && result !== 'http://') {
|
|
||||||
run(this, function () {
|
|
||||||
this.sendAction('uploaded', result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
el.on('imagecleared', run.bind(this, 'sendAction', 'canceled'));
|
|
||||||
|
|
||||||
this.sendAction('initUploader', ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,6 +1,5 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
import ModalComponent from 'ghost/components/modals/base';
|
import ModalComponent from 'ghost/components/modals/base';
|
||||||
import upload from 'ghost/assets/lib/uploader';
|
|
||||||
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
|
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -10,14 +9,16 @@ const {
|
|||||||
} = Ember;
|
} = Ember;
|
||||||
|
|
||||||
export default ModalComponent.extend({
|
export default ModalComponent.extend({
|
||||||
acceptEncoding: 'image/*',
|
|
||||||
model: null,
|
model: null,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
|
||||||
|
url: '',
|
||||||
|
newUrl: '',
|
||||||
|
|
||||||
config: service(),
|
config: service(),
|
||||||
notifications: service(),
|
notifications: service(),
|
||||||
|
|
||||||
imageUrl: computed('model.model', 'model.imageProperty', {
|
image: computed('model.model', 'model.imageProperty', {
|
||||||
get() {
|
get() {
|
||||||
let imageProperty = this.get('model.imageProperty');
|
let imageProperty = this.get('model.imageProperty');
|
||||||
|
|
||||||
@ -32,51 +33,62 @@ export default ModalComponent.extend({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
didInsertElement() {
|
didReceiveAttrs() {
|
||||||
this._super(...arguments);
|
let image = this.get('image');
|
||||||
upload.call(this.$('.js-drop-zone'), {
|
this.set('url', image);
|
||||||
fileStorage: this.get('config.fileStorage')
|
this.set('newUrl', image);
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: should validation be handled in the gh-image-uploader component?
|
||||||
|
// pro - consistency everywhere, simplification here
|
||||||
|
// con - difficult if the "save" is happening externally as it does here
|
||||||
|
//
|
||||||
|
// maybe it should be handled at the model level?
|
||||||
|
// - automatically present everywhere
|
||||||
|
// - file uploads should always result in valid urls so it should only
|
||||||
|
// affect the url input form
|
||||||
keyDown() {
|
keyDown() {
|
||||||
this._setErrorState(false);
|
this._setErrorState(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setErrorState(state) {
|
_setErrorState(state) {
|
||||||
if (state) {
|
if (state) {
|
||||||
this.$('.js-upload-url').addClass('error');
|
this.$('.url').addClass('error');
|
||||||
} else {
|
} else {
|
||||||
this.$('.js-upload-url').removeClass('error');
|
this.$('.url').removeClass('error');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_setImageProperty() {
|
_validateUrl(url) {
|
||||||
let value;
|
if (!isEmpty(url) && !cajaSanitizers.url(url)) {
|
||||||
|
this._setErrorState(true);
|
||||||
if (this.$('.js-upload-url').val()) {
|
return {message: 'Image URI is not valid'};
|
||||||
value = this.$('.js-upload-url').val();
|
|
||||||
|
|
||||||
if (!isEmpty(value) && !cajaSanitizers.url(value)) {
|
|
||||||
this._setErrorState(true);
|
|
||||||
return {message: 'Image URI is not valid'};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = this.$('.js-upload-target').attr('src');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('imageUrl', value);
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
// end validation
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
fileUploaded(url) {
|
||||||
|
this.set('url', url);
|
||||||
|
this.set('newUrl', url);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeImage() {
|
||||||
|
this.set('url', '');
|
||||||
|
this.set('newUrl', '');
|
||||||
|
},
|
||||||
|
|
||||||
confirm() {
|
confirm() {
|
||||||
let model = this.get('model.model');
|
let model = this.get('model.model');
|
||||||
|
let newUrl = this.get('newUrl');
|
||||||
|
let result = this._validateUrl(newUrl);
|
||||||
let notifications = this.get('notifications');
|
let notifications = this.get('notifications');
|
||||||
let result = this._setImageProperty();
|
|
||||||
|
|
||||||
if (!result.message) {
|
if (result === true) {
|
||||||
this.set('submitting', true);
|
this.set('submitting', true);
|
||||||
|
this.set('image', newUrl);
|
||||||
|
|
||||||
model.save().catch((err) => {
|
model.save().catch((err) => {
|
||||||
notifications.showAPIError(err, {key: 'image.upload'});
|
notifications.showAPIError(err, {key: 'image.upload'});
|
||||||
|
@ -24,7 +24,6 @@ export default Controller.extend(SettingsMenuMixin, {
|
|||||||
debounceId: null,
|
debounceId: null,
|
||||||
lastPromise: null,
|
lastPromise: null,
|
||||||
selectedAuthor: null,
|
selectedAuthor: null,
|
||||||
uploaderReference: null,
|
|
||||||
|
|
||||||
application: controller(),
|
application: controller(),
|
||||||
config: service(),
|
config: service(),
|
||||||
@ -408,14 +407,6 @@ export default Controller.extend(SettingsMenuMixin, {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
resetUploader() {
|
|
||||||
let uploader = this.get('uploaderReference');
|
|
||||||
|
|
||||||
if (uploader && uploader[0]) {
|
|
||||||
uploader[0].uploaderUi.reset();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
resetPubDate() {
|
resetPubDate() {
|
||||||
this.set('publishedAtValue', '');
|
this.set('publishedAtValue', '');
|
||||||
},
|
},
|
||||||
|
@ -131,6 +131,7 @@ export default Mixin.create({
|
|||||||
|
|
||||||
$textarea.focus();
|
$textarea.focus();
|
||||||
// Tell the editor it has changed, as programmatic replacements won't trigger this automatically
|
// Tell the editor it has changed, as programmatic replacements won't trigger this automatically
|
||||||
|
this._elementValueDidChange();
|
||||||
this.sendAction('onChange');
|
this.sendAction('onChange');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -32,8 +32,7 @@ export default AuthenticatedRoute.extend(base, {
|
|||||||
// from previous posts
|
// from previous posts
|
||||||
psm.removeObserver('titleScratch', psm, 'titleObserver');
|
psm.removeObserver('titleScratch', psm, 'titleObserver');
|
||||||
|
|
||||||
// Ensure that the PSM Image Uploader and Publish Date selector resets
|
// Ensure that the PSM Publish Date selector resets
|
||||||
psm.send('resetUploader');
|
|
||||||
psm.send('resetPubDate');
|
psm.send('resetPubDate');
|
||||||
|
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
import AjaxService from 'ember-ajax/services/ajax';
|
import AjaxService from 'ember-ajax/services/ajax';
|
||||||
|
import {AjaxError} from 'ember-ajax/errors';
|
||||||
|
|
||||||
const {inject, computed} = Ember;
|
const {inject, computed} = Ember;
|
||||||
|
|
||||||
|
export function RequestEntityTooLargeError(errors) {
|
||||||
|
AjaxError.call(this, errors, 'Request was rejected because it\'s larger than the maximum file size the server allows');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UnsupportedMediaTypeError(errors) {
|
||||||
|
AjaxError.call(this, errors, 'Request was rejected because it contains an unknown or unsupported file type.');
|
||||||
|
}
|
||||||
|
|
||||||
export default AjaxService.extend({
|
export default AjaxService.extend({
|
||||||
session: inject.service(),
|
session: inject.service(),
|
||||||
|
|
||||||
@ -22,11 +31,29 @@ export default AjaxService.extend({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
handleResponse(status, headers, payload) {
|
||||||
|
if (this.isRequestEntityTooLarge(status, headers, payload)) {
|
||||||
|
return new RequestEntityTooLargeError(payload.errors);
|
||||||
|
} else if (this.isUnsupportedMediaType(status, headers, payload)) {
|
||||||
|
return new UnsupportedMediaTypeError(payload.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._super(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
normalizeErrorResponse(status, headers, payload) {
|
normalizeErrorResponse(status, headers, payload) {
|
||||||
if (payload && typeof payload === 'object') {
|
if (payload && typeof payload === 'object') {
|
||||||
payload.errors = payload.error || payload.errors || payload.message || undefined;
|
payload.errors = payload.error || payload.errors || payload.message || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._super(status, headers, payload);
|
return this._super(status, headers, payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
isRequestEntityTooLarge(status/*, headers, payload */) {
|
||||||
|
return status === 413;
|
||||||
|
},
|
||||||
|
|
||||||
|
isUnsupportedMediaType(status/*, headers, payload */) {
|
||||||
|
return status === 415;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -61,8 +61,7 @@
|
|||||||
/* The modal
|
/* The modal
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
.fullscreen-modal .image-uploader,
|
.fullscreen-modal .gh-image-uploader {
|
||||||
.fullscreen-modal .pre-image-uploader {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,18 +102,25 @@
|
|||||||
padding: 0 24px 24px;
|
padding: 0 24px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-menu-content .image-uploader {
|
.settings-menu-content .gh-image-uploader {
|
||||||
margin: 0 0 1.6rem 0;
|
margin: 0 0 1.6rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-menu-content .image-uploader .description {
|
.settings-menu-content .gh-image-uploader .description {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-menu-content .image-uploader.image-uploader-url {
|
.settings-menu-content .gh-image-uploader form {
|
||||||
padding: 35px 45px;
|
padding: 35px 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-menu-content .gh-image-uploader.--with-image {
|
||||||
|
margin-top: 0;
|
||||||
|
min-height: 50px;
|
||||||
|
max-height: 250px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-menu-content textarea {
|
.settings-menu-content textarea {
|
||||||
height: 108px;
|
height: 108px;
|
||||||
}
|
}
|
||||||
@ -138,13 +145,6 @@
|
|||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-menu-content .pre-image-uploader {
|
|
||||||
margin-top: 0;
|
|
||||||
min-height: 50px;
|
|
||||||
max-height: 250px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-menu-content .word-count {
|
.settings-menu-content .word-count {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
@ -1,162 +1,38 @@
|
|||||||
/* Image Uploader
|
/* Image Uploader
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
.image-uploader {
|
.gh-image-uploader {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 1.6em 0;
|
margin: 1.6em 0;
|
||||||
padding: 55px 60px;
|
min-height: 130px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
|
||||||
background: #f6f7f8;
|
background: #f6f7f8;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #808284;
|
color: #808284;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-uploader .description {
|
.gh-image-uploader.--drag-over {
|
||||||
font-size: 1.6rem;
|
border: 2px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-uploader a {
|
.gh-image-uploader.--with-image {
|
||||||
color: var(--midgrey);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader a:hover {
|
|
||||||
color: var(--darkgrey);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .image-upload,
|
|
||||||
.image-uploader .image-url {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
display: block;
|
|
||||||
padding: 10px;
|
|
||||||
color: var(--midgrey);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .image-upload:hover,
|
|
||||||
.image-uploader .image-url:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .btn-green {
|
|
||||||
position: relative;
|
|
||||||
z-index: 700;
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 10px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader input.main {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 23px;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
transform: scale(14);
|
|
||||||
transform-origin: right;
|
|
||||||
direction: ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader input.main.right {
|
|
||||||
right: 9999px;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader input.url {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
padding: 9px 7px;
|
|
||||||
outline: 0;
|
|
||||||
background: #fff;
|
|
||||||
vertical-align: middle;
|
|
||||||
font: -webkit-small-control;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader input.url + .btn.btn-blue {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .progress {
|
|
||||||
position: relative;
|
|
||||||
top: 50%;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .fileupload-loading {
|
|
||||||
top: 50%;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 35px;
|
|
||||||
height: 28px;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .failed {
|
|
||||||
position: relative;
|
|
||||||
top: -40px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .bar {
|
|
||||||
height: 12px;
|
|
||||||
background: var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-uploader .bar.fail {
|
|
||||||
background: var(--red);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Pre-Image-Uploader // TODO: RENAME
|
|
||||||
/* ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
.pre-image-uploader {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 1.6em 0;
|
|
||||||
min-height: 46px;
|
|
||||||
height: auto;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
color: var(--midgrey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pre-image-uploader input {
|
.gh-image-uploader img {
|
||||||
position: absolute;
|
|
||||||
left: 9999px;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre-image-uploader a {
|
|
||||||
z-index: 10000;
|
|
||||||
color: var(--midgrey);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre-image-uploader a:hover {
|
|
||||||
color: var(--darkgrey);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre-image-uploader img {
|
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pre-image-uploader .image-cancel {
|
.gh-image-uploader .image-cancel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@ -172,8 +48,115 @@
|
|||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pre-image-uploader .image-cancel:hover {
|
.gh-image-uploader .upload-form {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .x-file-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .x-file-input label {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .description {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .image-upload,
|
||||||
|
.gh-image-uploader .image-url {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--midgrey);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader a {
|
||||||
|
color: var(--midgrey);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader a:hover {
|
||||||
|
color: var(--darkgrey);
|
||||||
|
}
|
||||||
|
.gh-image-uploader .image-upload:hover,
|
||||||
|
.gh-image-uploader .image-url:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader form {
|
||||||
|
padding: 55px 60px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader input.url {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
padding: 9px 7px;
|
||||||
|
outline: 0;
|
||||||
|
background: #fff;
|
||||||
|
vertical-align: middle;
|
||||||
|
font: -webkit-small-control;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader input.url + .btn.btn-blue {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .image-cancel:hover {
|
||||||
background: var(--red);
|
background: var(--red);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .progress-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .progress {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 60%;
|
||||||
|
background: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .failed {
|
||||||
|
margin: 1em 2em;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .bar {
|
||||||
|
height: 12px;
|
||||||
|
background: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader .bar.fail {
|
||||||
|
width: 100% !important;
|
||||||
|
background: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Try Again button */
|
||||||
|
.gh-image-uploader .btn-green:last-child {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
}
|
||||||
|
@ -1 +1,14 @@
|
|||||||
{{gh-format-markdown markdown}}
|
{{gh-format-markdown markdown}}
|
||||||
|
|
||||||
|
{{#each imageUploadComponents as |uploader|}}
|
||||||
|
{{#ember-wormhole to=uploader.destinationElementId}}
|
||||||
|
{{gh-image-uploader-with-preview
|
||||||
|
image=uploader.src
|
||||||
|
text="Upload an image"
|
||||||
|
update=(action "updateImageSrc" uploader.index)
|
||||||
|
remove=(action "updateImageSrc" uploader.index "")
|
||||||
|
uploadStarted=uploadStarted
|
||||||
|
uploadFinished=uploadFinished
|
||||||
|
formChanged=(action "updateHeight")}}
|
||||||
|
{{/ember-wormhole}}
|
||||||
|
{{/each}}
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
updateHeight=(action "updateHeight")
|
updateHeight=(action "updateHeight")
|
||||||
uploadStarted=(action "disableEditor")
|
uploadStarted=(action "disableEditor")
|
||||||
uploadFinished=(action "enableEditor")
|
uploadFinished=(action "enableEditor")
|
||||||
uploadSuccess=(action "handleImgUpload")}}
|
updateImageSrc=(action "handleImgUpload")}}
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
{{#if image}}
|
||||||
|
<div class="gh-image-uploader --with-image">
|
||||||
|
<div><img src={{image}}></div>
|
||||||
|
<a class="image-cancel icon-trash" title="Delete" {{action remove}}>
|
||||||
|
<span class="hidden">Delete</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
{{gh-image-uploader
|
||||||
|
text=text
|
||||||
|
update=(action 'update')
|
||||||
|
onInput=(action 'onInput')
|
||||||
|
uploadStarted=(action 'uploadStarted')
|
||||||
|
uploadFinished=(action 'uploadFinished')
|
||||||
|
formChanged=(action 'formChanged')}}
|
||||||
|
{{/if}}
|
43
ghost/admin/app/templates/components/gh-image-uploader.hbs
Normal file
43
ghost/admin/app/templates/components/gh-image-uploader.hbs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{{#if file}}
|
||||||
|
{{!-- Upload in progress! --}}
|
||||||
|
{{#if failureMessage}}
|
||||||
|
<div class="failed">{{failureMessage}}</div>
|
||||||
|
{{/if}}
|
||||||
|
<div class="progress-container">
|
||||||
|
<div class="progress">
|
||||||
|
<div class="bar {{if failureMessage "fail"}}" style={{progressStyle}}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{#if failureMessage}}
|
||||||
|
<button class="btn btn-green" {{action "reset"}}>Try Again</button>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{#if showUploadForm}}
|
||||||
|
{{!-- file selection/drag-n-drop --}}
|
||||||
|
<div class="upload-form">
|
||||||
|
{{#x-file-input multiple=false alt=text action=(action 'fileSelected') accept="image/gif,image/jpg,image/jpeg,image/png,image/svg+xml"}}
|
||||||
|
<div class="description">{{text}}</div>
|
||||||
|
{{/x-file-input}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a class="image-url" {{action 'switchForm' 'url-input'}}>
|
||||||
|
<i class="icon-link"><span class="hidden">URL</span></i>
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
{{!-- URL input --}}
|
||||||
|
<form class="url-form">
|
||||||
|
{{one-way-input class="url gh-input" placeholder="http://" value=url update=(action "onInput") onenter=(action "saveUrl")}}
|
||||||
|
{{#if saveButton}}
|
||||||
|
<button class="btn btn-blue gh-input" {{action 'saveUrl'}}>Save</button>
|
||||||
|
{{else}}
|
||||||
|
<div class="description">{{text}}</div>
|
||||||
|
{{/if}}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{#if canShowUploadForm}}
|
||||||
|
<a class="image-upload icon-photos" title="Add image" {{action 'switchForm' 'upload'}}>
|
||||||
|
<span class="hidden">Upload</span>
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
@ -9,7 +9,11 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-menu-content">
|
<div class="settings-menu-content">
|
||||||
{{gh-uploader uploaded="setCoverImage" canceled="clearCoverImage" description="Add tag image" image=tag.image initUploader="setUploaderReference" tagName="section"}}
|
{{gh-image-uploader-with-preview
|
||||||
|
image=tag.image
|
||||||
|
text="Add tag image"
|
||||||
|
update=(action "setCoverImage")
|
||||||
|
remove=(action "clearCoverImage")}}
|
||||||
<form>
|
<form>
|
||||||
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="name"}}
|
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="name"}}
|
||||||
<label for="tag-name">Name</label>
|
<label for="tag-name">Name</label>
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
<img class="js-upload-target" src="{{imageSource}}" />
|
|
||||||
<div class="description">{{description}}<strong></strong></div>
|
|
||||||
<input data-url="upload" class="gh-input js-fileupload main fileupload" type="file" name="uploadimage">
|
|
@ -1,8 +1,17 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<section class="js-drop-zone">
|
{{#if url}}
|
||||||
<img class="js-upload-target" src="{{imageUrl}}" alt="logo">
|
<div class="gh-image-uploader --with-image">
|
||||||
<input data-url="upload" class="js-fileupload main" type="file" name="uploadimage" accept="{{acceptEncoding}}">
|
<div><img src={{url}}></div>
|
||||||
</section>
|
<a class="image-cancel icon-trash" title="Delete" {{action 'removeImage'}}>
|
||||||
|
<span class="hidden">Delete</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
{{gh-image-uploader image=newUrl
|
||||||
|
saveButton=false
|
||||||
|
update=(action 'fileUploaded')
|
||||||
|
onInput=(action (mut newUrl))}}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
@ -6,7 +6,11 @@
|
|||||||
<button class="close icon-x settings-menu-header-action" {{action "closeMenus"}}><span class="hidden">Close</span></button>
|
<button class="close icon-x settings-menu-header-action" {{action "closeMenus"}}><span class="hidden">Close</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-menu-content">
|
<div class="settings-menu-content">
|
||||||
{{gh-uploader uploaded="setCoverImage" canceled="clearCoverImage" description="Add post image" image=model.image uploaderReference=uploaderReference tagName="section"}}
|
{{gh-image-uploader-with-preview
|
||||||
|
image=model.image
|
||||||
|
text="Add post image"
|
||||||
|
update=(action "setCoverImage")
|
||||||
|
remove=(action "clearCoverImage")}}
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="url">Post URL</label>
|
<label for="url">Post URL</label>
|
||||||
|
@ -12,23 +12,9 @@ function parse(stringToParse) {
|
|||||||
return images;
|
return images;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all dropzones in the preview and find which one was the target of the upload
|
|
||||||
function getZoneIndex(element) {
|
|
||||||
let zones = document.querySelectorAll('.js-entry-preview .js-drop-zone');
|
|
||||||
|
|
||||||
for (let i = 0; i < zones.length; i += 1) {
|
|
||||||
if (zones.item(i) === element) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out the start and end of the selection range for the src in the markdown, so we know what to replace
|
// Figure out the start and end of the selection range for the src in the markdown, so we know what to replace
|
||||||
function getSrcRange(content, element) {
|
function getSrcRange(content, index) {
|
||||||
let images = parse(content);
|
let images = parse(content);
|
||||||
let index = getZoneIndex(element);
|
|
||||||
let replacement = {};
|
let replacement = {};
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"ember-export-application-global": "1.0.5",
|
"ember-export-application-global": "1.0.5",
|
||||||
"ember-load-initializers": "0.5.1",
|
"ember-load-initializers": "0.5.1",
|
||||||
"ember-myth": "0.1.1",
|
"ember-myth": "0.1.1",
|
||||||
|
"ember-one-way-controls": "0.5.3",
|
||||||
"ember-resolver": "2.0.3",
|
"ember-resolver": "2.0.3",
|
||||||
"ember-route-action-helper": "0.3.0",
|
"ember-route-action-helper": "0.3.0",
|
||||||
"ember-simple-auth": "1.0.1",
|
"ember-simple-auth": "1.0.1",
|
||||||
@ -49,6 +50,8 @@
|
|||||||
"ember-sortable": "1.7.0",
|
"ember-sortable": "1.7.0",
|
||||||
"ember-suave": "2.0.1",
|
"ember-suave": "2.0.1",
|
||||||
"ember-watson": "0.7.0",
|
"ember-watson": "0.7.0",
|
||||||
|
"ember-wormhole": "0.3.5",
|
||||||
|
"emberx-file-input": "1.0.0",
|
||||||
"fs-extra": "0.16.3",
|
"fs-extra": "0.16.3",
|
||||||
"glob": "^4.0.5",
|
"glob": "^4.0.5",
|
||||||
"liquid-fire": "0.23.0",
|
"liquid-fire": "0.23.0",
|
||||||
|
@ -92,13 +92,13 @@ describe('Acceptance: Settings - General', function () {
|
|||||||
click('.blog-logo');
|
click('.blog-logo');
|
||||||
|
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
expect(find('.fullscreen-modal .modal-content .js-drop-zone').length, 'modal selector').to.equal(1);
|
expect(find('.fullscreen-modal .modal-content .gh-image-uploader').length, 'modal selector').to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
click('.fullscreen-modal .modal-content .js-drop-zone .js-cancel');
|
click('.fullscreen-modal .modal-content .gh-image-uploader .image-cancel');
|
||||||
|
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
expect(find('.fullscreen-modal .modal-content .js-drop-zone .description').text()).to.equal('Add image');
|
expect(find('.fullscreen-modal .modal-content .gh-image-uploader .description').text()).to.equal('Upload an image');
|
||||||
});
|
});
|
||||||
|
|
||||||
// click cancel button
|
// click cancel button
|
||||||
@ -111,7 +111,7 @@ describe('Acceptance: Settings - General', function () {
|
|||||||
click('.blog-cover');
|
click('.blog-cover');
|
||||||
|
|
||||||
andThen(() => {
|
andThen(() => {
|
||||||
expect(find('.fullscreen-modal .modal-content .js-drop-zone').length, 'modal selector').to.equal(1);
|
expect(find('.fullscreen-modal .modal-content .gh-image-uploader').length, 'modal selector').to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
click('.fullscreen-modal .modal-footer .js-button-accept');
|
click('.fullscreen-modal .modal-footer .js-button-accept');
|
||||||
|
@ -0,0 +1,239 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import Ember from 'ember';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describeComponent,
|
||||||
|
it
|
||||||
|
} from 'ember-mocha';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import Pretender from 'pretender';
|
||||||
|
import wait from 'ember-test-helpers/wait';
|
||||||
|
|
||||||
|
const {run} = Ember;
|
||||||
|
|
||||||
|
const keyCodes = {
|
||||||
|
enter: 13
|
||||||
|
};
|
||||||
|
|
||||||
|
const configStub = Ember.Service.extend({
|
||||||
|
fileStorage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionStub = Ember.Service.extend({
|
||||||
|
isAuthenticated: false,
|
||||||
|
authorize(authorizer, block) {
|
||||||
|
if (this.get('isAuthenticated')) {
|
||||||
|
block('Authorization', 'Bearer token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||||
|
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||||
|
return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"'];
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stubFailedUpload = function (server, code, error, delay = 0) {
|
||||||
|
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||||
|
return [code, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||||
|
errors: [{
|
||||||
|
errorType: error,
|
||||||
|
message: `Error: ${error}`
|
||||||
|
}]
|
||||||
|
})];
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
describeComponent(
|
||||||
|
'gh-image-upload',
|
||||||
|
'Integration: Component: gh-image-uploader',
|
||||||
|
{
|
||||||
|
integration: true
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
let server;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
this.register('service:config', configStub);
|
||||||
|
this.register('service:session', sessionStub);
|
||||||
|
this.inject.service('config', {as: 'configService'});
|
||||||
|
this.inject.service('session', {as: 'sessionService'});
|
||||||
|
this.set('update', function () {});
|
||||||
|
server = new Pretender();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', function() {
|
||||||
|
this.set('image', 'http://example.com/test.png');
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image}}`);
|
||||||
|
expect(this.$()).to.have.length(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to upload form', function () {
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image}}`);
|
||||||
|
expect(this.$('input[type="file"]').length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to url form with no filestorage config', function () {
|
||||||
|
this.set('configService.fileStorage', false);
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image}}`);
|
||||||
|
expect(this.$('input[type="file"]').length).to.equal(0);
|
||||||
|
expect(this.$('input[type="text"].url').length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can switch between form types', function () {
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image}}`);
|
||||||
|
expect(this.$('input[type="file"]').length).to.equal(1);
|
||||||
|
expect(this.$('input[type="text"].url').length).to.equal(0);
|
||||||
|
|
||||||
|
this.$('a.image-url').click();
|
||||||
|
|
||||||
|
expect(this.$('input[type="file"]').length, 'upload form is visible after switch to url form')
|
||||||
|
.to.equal(0);
|
||||||
|
expect(this.$('input[type="text"].url').length, 'url form is visible after switch to url form')
|
||||||
|
.to.equal(1);
|
||||||
|
|
||||||
|
this.$('a.image-upload').click();
|
||||||
|
|
||||||
|
expect(this.$('input[type="file"]').length, 'upload form is visible after switch to upload form')
|
||||||
|
.to.equal(1);
|
||||||
|
expect(this.$('input[type="text"].url').length, 'url form is visible after switch to upload form')
|
||||||
|
.to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers formChanged action when switching between forms', function () {
|
||||||
|
let formChanged = sinon.spy();
|
||||||
|
this.set('formChanged', formChanged);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image formChanged=(action formChanged)}}`);
|
||||||
|
|
||||||
|
this.$('a.image-url').click();
|
||||||
|
this.$('a.image-upload').click();
|
||||||
|
|
||||||
|
expect(formChanged.calledTwice).to.be.true;
|
||||||
|
expect(formChanged.firstCall.args[0]).to.equal('url-input');
|
||||||
|
expect(formChanged.secondCall.args[0]).to.equal('upload');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('file uploads', function () {
|
||||||
|
it('renders form with supplied text', function () {
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image text="text test"}}`);
|
||||||
|
expect(this.$('.description').text().trim()).to.equal('text test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates request to correct endpoint', function (done) {
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||||
|
this.$('input[type="file"]').trigger('change');
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(server.handledRequests.length).to.equal(1);
|
||||||
|
expect(server.handledRequests[0].url).to.equal('/ghost/api/v0.1/uploads/');
|
||||||
|
expect(server.handledRequests[0].requestHeaders.Authorization).to.be.undefined;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds authentication headers to request', function (done) {
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.get('sessionService').set('isAuthenticated', true);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||||
|
this.$('input[type="file"]').trigger('change');
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
let [request] = server.handledRequests;
|
||||||
|
expect(request.requestHeaders.Authorization).to.equal('Bearer token');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles drag over/leave', function () {
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
this.$('.gh-image-uploader').trigger('dragover');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(this.$('.gh-image-uploader').hasClass('--drag-over'), 'has drag-over class').to.be.true;
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
this.$('.gh-image-uploader').trigger('dragleave');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(this.$('.gh-image-uploader').hasClass('--drag-over'), 'has drag-over class').to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL input', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
this.set('configService.fileStorage', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays save button by default', function () {
|
||||||
|
this.set('image', 'http://example.com/test.png');
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image text="text test"}}`);
|
||||||
|
expect(this.$('button').length).to.equal(1);
|
||||||
|
expect(this.$('input[type="text"]').val()).to.equal('http://example.com/test.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can render without a save button', function () {
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image saveButton=false text="text test"}}`);
|
||||||
|
expect(this.$('button').length).to.equal(0);
|
||||||
|
expect(this.$('.description').text().trim()).to.equal('text test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires update action when save button clicked', function () {
|
||||||
|
let update = sinon.spy();
|
||||||
|
this.set('update', update);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||||
|
|
||||||
|
this.$('input[type="text"]').val('saved url');
|
||||||
|
this.$('input[type="text"]').change();
|
||||||
|
this.$('button.btn-blue').click();
|
||||||
|
|
||||||
|
expect(update.calledOnce).to.be.true;
|
||||||
|
expect(update.firstCall.args[0]).to.equal('saved url');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires onInput action when typing URL', function () {
|
||||||
|
let onInput = sinon.spy();
|
||||||
|
this.set('onInput', onInput);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image onInput=(action onInput)}}`);
|
||||||
|
|
||||||
|
this.$('input[type="text"]').val('input url');
|
||||||
|
this.$('input[type="text"]').change();
|
||||||
|
|
||||||
|
expect(onInput.calledOnce).to.be.true;
|
||||||
|
expect(onInput.firstCall.args[0]).to.equal('input url');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves on enter key', function () {
|
||||||
|
let update = sinon.spy();
|
||||||
|
this.set('update', update);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader image=image update=(action update)}}`);
|
||||||
|
|
||||||
|
this.$('input[type="text"]').val('saved url');
|
||||||
|
this.$('input[type="text"]').change();
|
||||||
|
this.$('input[type="text"]').trigger(
|
||||||
|
$.Event('keyup', {keyCode: keyCodes.enter, which: keyCodes.enter})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(update.calledOnce).to.be.true;
|
||||||
|
expect(update.firstCall.args[0]).to.equal('saved url');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,48 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describeComponent,
|
||||||
|
it
|
||||||
|
} from 'ember-mocha';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import Ember from 'ember';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
const {run} = Ember;
|
||||||
|
|
||||||
|
describeComponent(
|
||||||
|
'gh-image-uploader-with-preview',
|
||||||
|
'Integration: Component: gh-image-uploader-with-preview',
|
||||||
|
{
|
||||||
|
integration: true
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
it('renders image if provided', function() {
|
||||||
|
this.set('image', 'http://example.com/test.png');
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader-with-preview image=image}}`);
|
||||||
|
|
||||||
|
expect(this.$('.gh-image-uploader.--with-image').length).to.equal(1);
|
||||||
|
expect(this.$('img').attr('src')).to.equal('http://example.com/test.png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders upload form when no image provided', function () {
|
||||||
|
this.render(hbs`{{gh-image-uploader-with-preview image=image}}`);
|
||||||
|
|
||||||
|
expect(this.$('input[type="file"]').length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers remove action when delete icon is clicked', function () {
|
||||||
|
let remove = sinon.spy();
|
||||||
|
this.set('remove', remove);
|
||||||
|
this.set('image', 'http://example.com/test.png');
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-image-uploader-with-preview image=image remove=remove}}`);
|
||||||
|
run(() => {
|
||||||
|
this.$('.icon-trash').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(remove.calledOnce).to.be.true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
@ -75,7 +75,7 @@ describeComponent(
|
|||||||
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
{{gh-tag-settings-form tag=tag setProperty=(action 'setProperty')}}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
expect(this.$('.image-uploader').length, 'displays image uploader').to.equal(1);
|
expect(this.$('.gh-image-uploader').length, 'displays image uploader').to.equal(1);
|
||||||
expect(this.$('input[name="name"]').val(), 'name field value').to.equal('Test');
|
expect(this.$('input[name="name"]').val(), 'name field value').to.equal('Test');
|
||||||
expect(this.$('input[name="slug"]').val(), 'slug field value').to.equal('test');
|
expect(this.$('input[name="slug"]').val(), 'slug field value').to.equal('test');
|
||||||
expect(this.$('textarea[name="description"]').val(), 'description field value').to.equal('Description.');
|
expect(this.$('textarea[name="description"]').val(), 'description field value').to.equal('Description.');
|
||||||
|
@ -5,11 +5,12 @@ import {
|
|||||||
} from 'ember-mocha';
|
} from 'ember-mocha';
|
||||||
import Pretender from 'pretender';
|
import Pretender from 'pretender';
|
||||||
import {AjaxError, UnauthorizedError} from 'ember-ajax/errors';
|
import {AjaxError, UnauthorizedError} from 'ember-ajax/errors';
|
||||||
|
import {RequestEntityTooLargeError, UnsupportedMediaTypeError} from 'ghost/services/ajax';
|
||||||
|
|
||||||
function stubAjaxEndpoint(server, response) {
|
function stubAjaxEndpoint(server, response = {}, code = 500) {
|
||||||
server.get('/test/', function () {
|
server.get('/test/', function () {
|
||||||
return [
|
return [
|
||||||
500,
|
code,
|
||||||
{'Content-Type': 'application/json'},
|
{'Content-Type': 'application/json'},
|
||||||
JSON.stringify(response)
|
JSON.stringify(response)
|
||||||
];
|
];
|
||||||
@ -89,13 +90,7 @@ describeModule(
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns known error object for built-in errors', function (done) {
|
it('returns known error object for built-in errors', function (done) {
|
||||||
server.get('/test/', function () {
|
stubAjaxEndpoint(server, '', 401);
|
||||||
return [
|
|
||||||
401,
|
|
||||||
{'Content-Type': 'application/json'},
|
|
||||||
''
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
let ajax = this.subject();
|
let ajax = this.subject();
|
||||||
|
|
||||||
@ -106,5 +101,31 @@ describeModule(
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns RequestEntityTooLargeError object for 413 errors', function (done) {
|
||||||
|
stubAjaxEndpoint(server, {}, 413);
|
||||||
|
|
||||||
|
let ajax = this.subject();
|
||||||
|
|
||||||
|
ajax.request('/test/').then(() => {
|
||||||
|
expect(false).to.be.true;
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error).to.be.instanceOf(RequestEntityTooLargeError);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns UnsupportedMediaTypeError object for 415 errors', function (done) {
|
||||||
|
stubAjaxEndpoint(server, {}, 415);
|
||||||
|
|
||||||
|
let ajax = this.subject();
|
||||||
|
|
||||||
|
ajax.request('/test/').then(() => {
|
||||||
|
expect(false).to.be.true;
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error).to.be.instanceOf(UnsupportedMediaTypeError);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
343
ghost/admin/tests/unit/components/gh-image-uploader-test.js
Normal file
343
ghost/admin/tests/unit/components/gh-image-uploader-test.js
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
/* global Blob */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {
|
||||||
|
describeComponent,
|
||||||
|
it
|
||||||
|
} from 'ember-mocha';
|
||||||
|
import Ember from 'ember';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import Pretender from 'pretender';
|
||||||
|
import wait from 'ember-test-helpers/wait';
|
||||||
|
|
||||||
|
const {run} = Ember;
|
||||||
|
|
||||||
|
const createFile = function (content = ['test'], options = {}) {
|
||||||
|
let {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
lastModifiedDate
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let file = new Blob(content, {type: type ? type : 'text/plain'});
|
||||||
|
file.name = name ? name : 'text.txt';
|
||||||
|
|
||||||
|
return file;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stubSuccessfulUpload = function (server, delay = 0) {
|
||||||
|
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||||
|
return [200, {'Content-Type': 'application/json'}, '"/content/images/test.png"'];
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stubFailedUpload = function (server, code, error, delay = 0) {
|
||||||
|
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||||
|
return [code, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||||
|
errors: [{
|
||||||
|
errorType: error,
|
||||||
|
message: `Error: ${error}`
|
||||||
|
}]
|
||||||
|
})];
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
describeComponent(
|
||||||
|
'gh-image-uploader',
|
||||||
|
'Unit: Component: gh-image-uploader',
|
||||||
|
{
|
||||||
|
needs: [
|
||||||
|
'service:config',
|
||||||
|
'service:session',
|
||||||
|
'service:ajax',
|
||||||
|
'component:x-file-input',
|
||||||
|
'component:one-way-input'
|
||||||
|
],
|
||||||
|
unit: true
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
let server;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
server = new Pretender();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', function() {
|
||||||
|
// creates the component instance
|
||||||
|
let component = this.subject();
|
||||||
|
// renders the component on the page
|
||||||
|
this.render();
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
expect(this.$()).to.have.length(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires update action on successful upload', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let update = sinon.spy();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = update;
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(update.calledOnce).to.be.true;
|
||||||
|
expect(update.firstCall.args[0]).to.equal('/content/images/test.png');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires uploadStarted action on upload start', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let uploadStarted = sinon.spy();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
component.attrs.uploadStarted = uploadStarted;
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(uploadStarted.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires uploadFinished action on successful upload', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let uploadFinished = sinon.spy();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
component.attrs.uploadFinished = uploadFinished;
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(uploadFinished.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires uploadFinished action on failed upload', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let uploadFinished = sinon.spy();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubFailedUpload(server);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
component.attrs.uploadFinished = uploadFinished;
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(uploadFinished.calledOnce).to.be.true;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays invalid file type error', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubFailedUpload(server, 415, 'UnsupportedMediaTypeError');
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||||
|
expect(this.$('.failed').text()).to.match(/The image type you uploaded is not supported/);
|
||||||
|
expect(this.$('.btn-green').length, 'reset button is displayed').to.equal(1);
|
||||||
|
expect(this.$('.btn-green').text()).to.equal('Try Again');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays file too large for server error', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubFailedUpload(server, 413, 'RequestEntityTooLargeError');
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||||
|
expect(this.$('.failed').text()).to.match(/The image you uploaded was larger/);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles file too large error directly from the web server', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||||
|
return [413, {}, ''];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||||
|
expect(this.$('.failed').text()).to.match(/The image you uploaded was larger/);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays other server-side error with message', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubFailedUpload(server, 400, 'UnknownError');
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||||
|
expect(this.$('.failed').text()).to.match(/Error: UnknownError/);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unknown failure', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
server.post('/ghost/api/v0.1/uploads/', function () {
|
||||||
|
return [500, {'Content-Type': 'application/json'}, ''];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(this.$('.failed').length, 'error message is displayed').to.equal(1);
|
||||||
|
expect(this.$('.failed').text()).to.match(/Something went wrong/);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be reset after a failed upload', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
stubFailedUpload(server, 400, 'UnknownError');
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
run(() => {
|
||||||
|
this.$('.btn-green').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(this.$('input[type="file"]').length).to.equal(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays upload progress', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
|
||||||
|
// pretender fires a progress event every 50ms
|
||||||
|
stubSuccessfulUpload(server, 150);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = () => {};
|
||||||
|
component.attrs.uploadFinished = done;
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
component.send('fileSelected', [file]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// after 75ms we should have had one progress event
|
||||||
|
run.later(this, function () {
|
||||||
|
expect(this.$('.progress .bar').length).to.equal(1);
|
||||||
|
let [_, percentageWidth] = this.$('.progress .bar').attr('style').match(/width: (\d+)%?/);
|
||||||
|
expect(percentageWidth).to.be.above(0);
|
||||||
|
expect(percentageWidth).to.be.below(100);
|
||||||
|
}, 75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers file upload on file drop', function (done) {
|
||||||
|
let component = this.subject();
|
||||||
|
let file = createFile();
|
||||||
|
let update = sinon.spy();
|
||||||
|
let drop = Ember.$.Event('drop', {
|
||||||
|
dataTransfer: {
|
||||||
|
files: [file]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stubSuccessfulUpload(server);
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
component.attrs.update = update;
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
this.$().trigger(drop);
|
||||||
|
});
|
||||||
|
|
||||||
|
wait().then(() => {
|
||||||
|
expect(update.calledOnce).to.be.true;
|
||||||
|
expect(update.firstCall.args[0]).to.equal('/content/images/test.png');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user