mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 11:55:03 +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 uploader from 'ghost/assets/lib/uploader';
|
||||
|
||||
const {
|
||||
$,
|
||||
Component,
|
||||
inject: {service},
|
||||
run
|
||||
run,
|
||||
uuid
|
||||
} = Ember;
|
||||
|
||||
export default Component.extend({
|
||||
config: service(),
|
||||
|
||||
_scrollWrapper: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('imageUploadComponents', Ember.A([]));
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this._scrollWrapper = this.$().closest('.entry-preview-content');
|
||||
this.adjustScrollPosition(this.get('scrollPosition'));
|
||||
run.scheduleOnce('afterRender', this, this.dropzoneHandler);
|
||||
run.scheduleOnce('afterRender', this, this.registerImageUploadComponents);
|
||||
},
|
||||
|
||||
didReceiveAttrs(attrs) {
|
||||
@ -32,7 +34,14 @@ export default Component.extend({
|
||||
}
|
||||
|
||||
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() {
|
||||
let dropzones = $('.js-drop-zone[data-uploaderui!="true"]');
|
||||
registerImageUploadComponents() {
|
||||
let dropzones = $('.js-drop-zone');
|
||||
|
||||
if (dropzones.length) {
|
||||
uploader.call(dropzones, {
|
||||
editor: true,
|
||||
fileStorage: this.get('config.fileStorage')
|
||||
dropzones.each((i, el) => {
|
||||
let id = uuid();
|
||||
let destinationElementId = `image-uploader-${id}`;
|
||||
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'));
|
||||
dropzones.on('uploadfailure', run.bind(this, 'sendAction', 'uploadFinished'));
|
||||
dropzones.on('uploadsuccess', run.bind(this, 'sendAction', 'uploadFinished'));
|
||||
dropzones.on('uploadsuccess', run.bind(this, 'sendAction', 'uploadSuccess'));
|
||||
el.id = destinationElementId;
|
||||
$(el).empty();
|
||||
$(el).removeClass('image-uploader');
|
||||
|
||||
// Set the current height so we can listen
|
||||
this.sendAction('updateHeight', this.$().height());
|
||||
run.schedule('afterRender', () => {
|
||||
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
|
||||
// ensuring that everything ends up in the correct place and format.
|
||||
handleImgUpload(e, resultSrc) {
|
||||
handleImgUpload(imageIndex, newSrc) {
|
||||
let editor = this.get('editor');
|
||||
let editorValue = editor.getValue();
|
||||
let replacement = imageManager.getSrcRange(editorValue, e.target);
|
||||
let replacement = imageManager.getSrcRange(editorValue, imageIndex);
|
||||
let cursorPosition;
|
||||
|
||||
if (replacement) {
|
||||
cursorPosition = replacement.start + resultSrc.length + 1;
|
||||
cursorPosition = replacement.start + newSrc.length + 1;
|
||||
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', '');
|
||||
},
|
||||
|
||||
setUploaderReference() {
|
||||
// noop
|
||||
},
|
||||
|
||||
openMeta() {
|
||||
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 ModalComponent from 'ghost/components/modals/base';
|
||||
import upload from 'ghost/assets/lib/uploader';
|
||||
import cajaSanitizers from 'ghost/utils/caja-sanitizers';
|
||||
|
||||
const {
|
||||
@ -10,14 +9,16 @@ const {
|
||||
} = Ember;
|
||||
|
||||
export default ModalComponent.extend({
|
||||
acceptEncoding: 'image/*',
|
||||
model: null,
|
||||
submitting: false,
|
||||
|
||||
url: '',
|
||||
newUrl: '',
|
||||
|
||||
config: service(),
|
||||
notifications: service(),
|
||||
|
||||
imageUrl: computed('model.model', 'model.imageProperty', {
|
||||
image: computed('model.model', 'model.imageProperty', {
|
||||
get() {
|
||||
let imageProperty = this.get('model.imageProperty');
|
||||
|
||||
@ -32,51 +33,62 @@ export default ModalComponent.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
upload.call(this.$('.js-drop-zone'), {
|
||||
fileStorage: this.get('config.fileStorage')
|
||||
});
|
||||
didReceiveAttrs() {
|
||||
let image = this.get('image');
|
||||
this.set('url', image);
|
||||
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() {
|
||||
this._setErrorState(false);
|
||||
},
|
||||
|
||||
_setErrorState(state) {
|
||||
if (state) {
|
||||
this.$('.js-upload-url').addClass('error');
|
||||
this.$('.url').addClass('error');
|
||||
} else {
|
||||
this.$('.js-upload-url').removeClass('error');
|
||||
this.$('.url').removeClass('error');
|
||||
}
|
||||
},
|
||||
|
||||
_setImageProperty() {
|
||||
let value;
|
||||
|
||||
if (this.$('.js-upload-url').val()) {
|
||||
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');
|
||||
_validateUrl(url) {
|
||||
if (!isEmpty(url) && !cajaSanitizers.url(url)) {
|
||||
this._setErrorState(true);
|
||||
return {message: 'Image URI is not valid'};
|
||||
}
|
||||
|
||||
this.set('imageUrl', value);
|
||||
return true;
|
||||
},
|
||||
// end validation
|
||||
|
||||
actions: {
|
||||
fileUploaded(url) {
|
||||
this.set('url', url);
|
||||
this.set('newUrl', url);
|
||||
},
|
||||
|
||||
removeImage() {
|
||||
this.set('url', '');
|
||||
this.set('newUrl', '');
|
||||
},
|
||||
|
||||
confirm() {
|
||||
let model = this.get('model.model');
|
||||
let newUrl = this.get('newUrl');
|
||||
let result = this._validateUrl(newUrl);
|
||||
let notifications = this.get('notifications');
|
||||
let result = this._setImageProperty();
|
||||
|
||||
if (!result.message) {
|
||||
if (result === true) {
|
||||
this.set('submitting', true);
|
||||
this.set('image', newUrl);
|
||||
|
||||
model.save().catch((err) => {
|
||||
notifications.showAPIError(err, {key: 'image.upload'});
|
||||
|
@ -24,7 +24,6 @@ export default Controller.extend(SettingsMenuMixin, {
|
||||
debounceId: null,
|
||||
lastPromise: null,
|
||||
selectedAuthor: null,
|
||||
uploaderReference: null,
|
||||
|
||||
application: controller(),
|
||||
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() {
|
||||
this.set('publishedAtValue', '');
|
||||
},
|
||||
|
@ -131,6 +131,7 @@ export default Mixin.create({
|
||||
|
||||
$textarea.focus();
|
||||
// Tell the editor it has changed, as programmatic replacements won't trigger this automatically
|
||||
this._elementValueDidChange();
|
||||
this.sendAction('onChange');
|
||||
}
|
||||
});
|
||||
|
@ -32,8 +32,7 @@ export default AuthenticatedRoute.extend(base, {
|
||||
// from previous posts
|
||||
psm.removeObserver('titleScratch', psm, 'titleObserver');
|
||||
|
||||
// Ensure that the PSM Image Uploader and Publish Date selector resets
|
||||
psm.send('resetUploader');
|
||||
// Ensure that the PSM Publish Date selector resets
|
||||
psm.send('resetPubDate');
|
||||
|
||||
this._super(...arguments);
|
||||
|
@ -1,8 +1,17 @@
|
||||
import Ember from 'ember';
|
||||
import AjaxService from 'ember-ajax/services/ajax';
|
||||
import {AjaxError} from 'ember-ajax/errors';
|
||||
|
||||
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({
|
||||
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) {
|
||||
if (payload && typeof payload === 'object') {
|
||||
payload.errors = payload.error || payload.errors || payload.message || undefined;
|
||||
}
|
||||
|
||||
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
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.fullscreen-modal .image-uploader,
|
||||
.fullscreen-modal .pre-image-uploader {
|
||||
.fullscreen-modal .gh-image-uploader {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -102,18 +102,25 @@
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.settings-menu-content .image-uploader {
|
||||
.settings-menu-content .gh-image-uploader {
|
||||
margin: 0 0 1.6rem 0;
|
||||
}
|
||||
|
||||
.settings-menu-content .image-uploader .description {
|
||||
.settings-menu-content .gh-image-uploader .description {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.settings-menu-content .image-uploader.image-uploader-url {
|
||||
.settings-menu-content .gh-image-uploader form {
|
||||
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 {
|
||||
height: 108px;
|
||||
}
|
||||
@ -138,13 +145,6 @@
|
||||
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 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -1,162 +1,38 @@
|
||||
/* Image Uploader
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
.image-uploader {
|
||||
.gh-image-uploader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
margin: 1.6em 0;
|
||||
padding: 55px 60px;
|
||||
min-height: 130px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: #f6f7f8;
|
||||
border-radius: 4px;
|
||||
color: #808284;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-uploader .description {
|
||||
font-size: 1.6rem;
|
||||
.gh-image-uploader.--drag-over {
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.image-uploader a {
|
||||
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;
|
||||
.gh-image-uploader.--with-image {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
color: var(--midgrey);
|
||||
}
|
||||
|
||||
.pre-image-uploader input {
|
||||
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 {
|
||||
.gh-image-uploader img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.pre-image-uploader .image-cancel {
|
||||
.gh-image-uploader .image-cancel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
@ -172,8 +48,115 @@
|
||||
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);
|
||||
color: #fff;
|
||||
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}}
|
||||
|
||||
{{#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")
|
||||
uploadStarted=(action "disableEditor")
|
||||
uploadFinished=(action "enableEditor")
|
||||
uploadSuccess=(action "handleImgUpload")}}
|
||||
updateImageSrc=(action "handleImgUpload")}}
|
||||
</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}}
|
||||
</div>
|
||||
<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>
|
||||
{{#gh-form-group errors=tag.errors hasValidated=tag.hasValidated property="name"}}
|
||||
<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">
|
||||
<section class="js-drop-zone">
|
||||
<img class="js-upload-target" src="{{imageUrl}}" alt="logo">
|
||||
<input data-url="upload" class="js-fileupload main" type="file" name="uploadimage" accept="{{acceptEncoding}}">
|
||||
</section>
|
||||
{{#if url}}
|
||||
<div class="gh-image-uploader --with-image">
|
||||
<div><img src={{url}}></div>
|
||||
<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 class="modal-footer">
|
||||
|
@ -6,7 +6,11 @@
|
||||
<button class="close icon-x settings-menu-header-action" {{action "closeMenus"}}><span class="hidden">Close</span></button>
|
||||
</div>
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<label for="url">Post URL</label>
|
||||
|
@ -12,23 +12,9 @@ function parse(stringToParse) {
|
||||
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
|
||||
function getSrcRange(content, element) {
|
||||
function getSrcRange(content, index) {
|
||||
let images = parse(content);
|
||||
let index = getZoneIndex(element);
|
||||
let replacement = {};
|
||||
|
||||
if (index > -1) {
|
||||
|
@ -42,6 +42,7 @@
|
||||
"ember-export-application-global": "1.0.5",
|
||||
"ember-load-initializers": "0.5.1",
|
||||
"ember-myth": "0.1.1",
|
||||
"ember-one-way-controls": "0.5.3",
|
||||
"ember-resolver": "2.0.3",
|
||||
"ember-route-action-helper": "0.3.0",
|
||||
"ember-simple-auth": "1.0.1",
|
||||
@ -49,6 +50,8 @@
|
||||
"ember-sortable": "1.7.0",
|
||||
"ember-suave": "2.0.1",
|
||||
"ember-watson": "0.7.0",
|
||||
"ember-wormhole": "0.3.5",
|
||||
"emberx-file-input": "1.0.0",
|
||||
"fs-extra": "0.16.3",
|
||||
"glob": "^4.0.5",
|
||||
"liquid-fire": "0.23.0",
|
||||
|
@ -92,13 +92,13 @@ describe('Acceptance: Settings - General', function () {
|
||||
click('.blog-logo');
|
||||
|
||||
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(() => {
|
||||
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
|
||||
@ -111,7 +111,7 @@ describe('Acceptance: Settings - General', function () {
|
||||
click('.blog-cover');
|
||||
|
||||
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');
|
||||
|
@ -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')}}
|
||||
`);
|
||||
|
||||
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="slug"]').val(), 'slug field value').to.equal('test');
|
||||
expect(this.$('textarea[name="description"]').val(), 'description field value').to.equal('Description.');
|
||||
|
@ -5,11 +5,12 @@ import {
|
||||
} from 'ember-mocha';
|
||||
import Pretender from 'pretender';
|
||||
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 () {
|
||||
return [
|
||||
500,
|
||||
code,
|
||||
{'Content-Type': 'application/json'},
|
||||
JSON.stringify(response)
|
||||
];
|
||||
@ -89,13 +90,7 @@ describeModule(
|
||||
});
|
||||
|
||||
it('returns known error object for built-in errors', function (done) {
|
||||
server.get('/test/', function () {
|
||||
return [
|
||||
401,
|
||||
{'Content-Type': 'application/json'},
|
||||
''
|
||||
];
|
||||
});
|
||||
stubAjaxEndpoint(server, '', 401);
|
||||
|
||||
let ajax = this.subject();
|
||||
|
||||
@ -106,5 +101,31 @@ describeModule(
|
||||
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