Merge pull request #758 from ErisDS/uploads-in-editor

Save image uploads in the editor
This commit is contained in:
Hannah Wolfe 2013-09-16 13:37:49 -07:00
commit 1870d00eef
3 changed files with 246 additions and 21 deletions

View File

@ -26,7 +26,7 @@
url: '/ghost/upload', url: '/ghost/upload',
add: function (e, data) { add: function (e, data) {
$progress.find('.js-upload-progress-bar').removeClass('fail'); $progress.find('.js-upload-progress-bar').removeClass('fail');
$dropzone.trigger('uploadstart'); $dropzone.trigger('uploadstart', [$dropzone.attr('id')]);
$dropzone.find('span.media, div.description, a.image-url, a.image-webcam') $dropzone.find('span.media, div.description, a.image-url, a.image-webcam')
.animate({opacity: 0}, 250, function () { .animate({opacity: 0}, 250, function () {
$dropzone.find('div.description').hide().css({"opacity": 100}); $dropzone.find('div.description').hide().css({"opacity": 100});
@ -47,6 +47,7 @@
} }
}, },
fail: function (e, data) { fail: function (e, data) {
$dropzone.trigger("uploadfailure", [data.result]);
$dropzone.find('.js-upload-progress-bar').addClass('fail'); $dropzone.find('.js-upload-progress-bar').addClass('fail');
$dropzone.find('div.js-fail, button.js-fail').fadeIn(1500); $dropzone.find('div.js-fail, button.js-fail').fadeIn(1500);
$dropzone.find('button.js-fail').on('click', function () { $dropzone.find('button.js-fail').on('click', function () {
@ -57,6 +58,8 @@
}); });
}, },
done: function (e, data) { done: function (e, data) {
$dropzone.trigger("uploadsuccess", [data.result, $dropzone.attr('id')]);
function showImage(width, height) { function showImage(width, height) {
$dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"}); $dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"});
$dropzone.find('.fileupload-loading').remove(); $dropzone.find('.fileupload-loading').remove();
@ -85,7 +88,6 @@
$dropzone.find('span.media').after('<img class="fileupload-loading" src="/public/img/loadingcat.gif" />'); $dropzone.find('span.media').after('<img class="fileupload-loading" src="/public/img/loadingcat.gif" />');
if (!settings.editor) {$progress.find('.fileupload-loading').css({"top": "56px"}); } if (!settings.editor) {$progress.find('.fileupload-loading').css({"top": "56px"}); }
}); });
$dropzone.trigger("uploadsuccess", [data.result]);
$img.one('load', function () { animateDropzone($img); }) $img.one('load', function () { animateDropzone($img); })
.attr('src', data.result); .attr('src', data.result);
} }

View File

@ -5,24 +5,18 @@
{ {
type: 'lang', type: 'lang',
filter: function (text) { filter: function (text) {
var defRegex = /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gim, var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
match, /* regex from isURL in node-validator. Yum! */
defUrls = {}; uriRegex = /^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i,
pathRegex = /^(\/)?([^\/\0]+(\/)?)+$/i;
while ((match = defRegex.exec(text)) !== null) { return text.replace(imageMarkdownRegex, function (match, key, alt, src) {
defUrls[match[1]] = match;
}
return text.replace(/^!(?:\[([^\n\]]*)\])(?:\[([^\n\]]*)\]|\(([^\n\]]*)\))?$/gim, function (match, alt, id, src) {
var result = ""; var result = "";
/* regex from isURL in node-validator. Yum! */ if (src && (src.match(uriRegex) || src.match(pathRegex))) {
if (src && src.match(/^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i)) {
result = '<img class="js-upload-target" src="' + src + '"/>'; result = '<img class="js-upload-target" src="' + src + '"/>';
} else if (id && defUrls.hasOwnProperty(id)) {
result = '<img class="js-upload-target" src="' + defUrls[id][2] + '"/>';
} }
return '<section class="js-drop-zone image-uploader">' + result + return '<section id="image_upload_' + key + '" class="js-drop-zone image-uploader">' + result +
'<div class="description">Add image of <strong>' + alt + '</strong></div>' + '<div class="description">Add image of <strong>' + alt + '</strong></div>' +
'<input data-url="upload" class="js-fileupload fileupload" type="file" name="uploadimage">' + '<input data-url="upload" class="js-fileupload fileupload" type="file" name="uploadimage">' +
'</section>'; '</section>';

View File

@ -4,8 +4,11 @@
(function () { (function () {
"use strict"; "use strict";
/*jslint regexp: true, bitwise: true */
var PublishBar, var PublishBar,
ActionsWidget, ActionsWidget,
UploadManager,
MarkerManager,
MarkdownShortcuts = [ MarkdownShortcuts = [
{'key': 'Ctrl+B', 'style': 'bold'}, {'key': 'Ctrl+B', 'style': 'bold'},
{'key': 'Meta+B', 'style': 'bold'}, {'key': 'Meta+B', 'style': 'bold'},
@ -31,7 +34,10 @@
{'key': 'Ctrl+L', 'style': 'list'}, {'key': 'Ctrl+L', 'style': 'list'},
{'key': 'Ctrl+Alt+C', 'style': 'copyHTML'}, {'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
{'key': 'Meta+Alt+C', 'style': 'copyHTML'} {'key': 'Meta+Alt+C', 'style': 'copyHTML'}
]; ],
imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
markerRegex = /\{<([\w\W]*?)>\}/;
/*jslint regexp: false, bitwise: false */
// The publish bar associated with a post, which has the TagWidget and // The publish bar associated with a post, which has the TagWidget and
// Save button and options and such. // Save button and options and such.
@ -197,14 +203,16 @@
}, },
savePost: function (data) { savePost: function (data) {
// TODO: The markdown getter here isn't great, shouldn't rely on currentView.
_.each(this.model.blacklist, function (item) { _.each(this.model.blacklist, function (item) {
this.model.unset(item); this.model.unset(item);
}, this); }, this);
var saved = this.model.save(_.extend({ var saved = this.model.save(_.extend({
title: $('#entry-title').val(), title: $('#entry-title').val(),
markdown: Ghost.currentView.editor.getValue() // TODO: The content_raw getter here isn't great, shouldn't rely on currentView.
markdown: Ghost.currentView.getEditorValue()
}, data)); }, data));
// TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone // TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
@ -260,7 +268,7 @@
}); });
// The entire /editor page's route (TODO: move all views to client side templates) // The entire /editor page's route
// ---------------------------------------- // ----------------------------------------
Ghost.Views.Editor = Ghost.View.extend({ Ghost.Views.Editor = Ghost.View.extend({
@ -371,7 +379,9 @@
var self = this, var self = this,
preview = document.getElementsByClassName('rendered-markdown')[0]; preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = this.converter.makeHtml(this.editor.getValue()); preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
this.$('.js-drop-zone').upload({editor: true});
this.initUploads();
Countable.once(preview, function (counter) { Countable.once(preview, function (counter) {
self.$('.entry-word-count').text($.pluralize(counter.words, 'word')); self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
self.$('.entry-character-count').text($.pluralize(counter.characters, 'character')); self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
@ -391,6 +401,7 @@
lineWrapping: true, lineWrapping: true,
dragDrop: false dragDrop: false
}); });
this.uploadMgr = new UploadManager(this.editor);
// Inject modal for HTML to be viewed in // Inject modal for HTML to be viewed in
shortcut.add("Ctrl+Alt+C", function () { shortcut.add("Ctrl+Alt+C", function () {
@ -406,11 +417,42 @@
}); });
}); });
this.enableEditor();
},
options: {
markers: {}
},
getEditorValue: function () {
return this.uploadMgr.getEditorValue();
},
initUploads: function () {
this.$('.js-drop-zone').upload({editor: true});
this.$('.js-drop-zone').on('uploadstart', $.proxy(this.disableEditor, this));
this.$('.js-drop-zone').on('uploadstart', this.uploadMgr.handleDownloadStart);
this.$('.js-drop-zone').on('uploadfailure', $.proxy(this.enableEditor, this));
this.$('.js-drop-zone').on('uploadsuccess', $.proxy(this.enableEditor, this));
this.$('.js-drop-zone').on('uploadsuccess', this.uploadMgr.handleDownloadSuccess);
},
enableEditor: function () {
var self = this;
this.editor.setOption("readOnly", false);
this.editor.on('change', function () { this.editor.on('change', function () {
self.renderPreview(); self.renderPreview();
}); });
}, },
disableEditor: function () {
var self = this;
this.editor.setOption("readOnly", "nocursor");
this.editor.off('change', function () {
self.renderPreview();
});
},
showHTML: function () { showHTML: function () {
this.addSubview(new Ghost.Views.Modal({ this.addSubview(new Ghost.Views.Modal({
model: { model: {
@ -431,4 +473,191 @@
render: function () { return this; } render: function () { return this; }
}); });
}()); MarkerManager = function (editor) {
var markers = {},
uploadPrefix = 'image_upload',
uploadId = 1;
function addMarker(line, ln) {
var marker,
magicId = '{<' + uploadId + '>}';
editor.setLine(ln, magicId + line.text);
marker = editor.markText(
{line: ln, ch: 0},
{line: ln, ch: (magicId.length)},
{collapsed: true}
);
markers[uploadPrefix + '_' + uploadId] = marker;
uploadId += 1;
}
function getMarkerRegexForId(id) {
id = id.replace('image_upload_', '');
return new RegExp('\\{<' + id + '>\\}', 'gmi');
}
function stripMarkerFromLine(line) {
var markerText = line.text.match(markerRegex),
ln = editor.getLineNumber(line);
if (markerText) {
editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
}
}
function findAndStripMarker(id) {
editor.eachLine(function (line) {
var markerText = getMarkerRegexForId(id).exec(line.text),
ln;
if (markerText) {
ln = editor.getLineNumber(line);
editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
}
});
}
function removeMarker(id, marker, line) {
delete markers[id];
marker.clear();
if (line) {
stripMarkerFromLine(line);
} else {
findAndStripMarker(id);
}
}
function checkMarkers() {
_.each(markers, function (marker, id) {
var line;
marker = markers[id];
if (marker.find()) {
line = editor.getLineHandle(marker.find().from.line);
if (!line.text.match(imageMarkdownRegex)) {
removeMarker(id, marker, line);
}
} else {
removeMarker(id, marker);
}
});
}
function initMarkers(line) {
var isImage = line.text.match(imageMarkdownRegex),
hasMarker = line.text.match(markerRegex);
if (isImage && !hasMarker) {
addMarker(line, editor.getLineNumber(line));
}
}
// public api
_.extend(this, {
markers: markers,
checkMarkers: checkMarkers,
addMarker: addMarker,
stripMarkerFromLine: stripMarkerFromLine,
getMarkerRegexForId: getMarkerRegexForId
});
// Initialise
editor.eachLine(initMarkers);
};
UploadManager = function (editor) {
var markerMgr = new MarkerManager(editor);
function findLine(result_id) {
// try to find the right line to replace
if (markerMgr.markers.hasOwnProperty(result_id) && markerMgr.markers[result_id].find()) {
return editor.getLineHandle(markerMgr.markers[result_id].find().from.line);
}
return false;
}
function checkLine(ln, mode) {
var line = editor.getLineHandle(ln),
isImage = line.text.match(imageMarkdownRegex),
hasMarker;
// We care if it is an image
if (isImage) {
hasMarker = line.text.match(markerRegex);
if (hasMarker && mode === 'paste') {
// this could be a duplicate, and won't be a real marker
markerMgr.stripMarkerFromLine(line);
}
if (!hasMarker) {
markerMgr.addMarker(line, ln);
}
}
// TODO: hasMarker but no image?
}
function handleDownloadStart(e) {
/*jslint regexp: true, bitwise: true */
var line = findLine($(e.currentTarget).attr('id')),
lineNumber = editor.getLineNumber(line),
match = line.text.match(/\([^\n]*\)?/),
replacement = '(http://)';
/*jslint regexp: false, bitwise: false */
if (match) {
// simple case, we have the parenthesis
editor.setSelection({line: lineNumber, ch: match.index + 1}, {line: lineNumber, ch: match.index + match[0].length - 1});
} else {
match = line.text.match(/\]/);
if (match) {
editor.replaceRange(
replacement,
{line: lineNumber, ch: match.index + 1},
{line: lineNumber, ch: match.index + 1}
);
editor.setSelection(
{line: lineNumber, ch: match.index + 2},
{line: lineNumber, ch: match.index + replacement.length }
);
}
}
}
function handleDownloadSuccess(e, result_src) {
editor.replaceSelection(result_src);
}
function getEditorValue() {
var value = editor.getValue();
_.each(markerMgr.markers, function (marker, id) {
value = value.replace(markerMgr.getMarkerRegexForId(id), '');
});
return value;
}
// Public API
_.extend(this, {
getEditorValue: getEditorValue,
handleDownloadStart: handleDownloadStart,
handleDownloadSuccess: handleDownloadSuccess
});
// initialise
editor.on('change', function (cm, changeObj) {
var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
_.each(linesChanged, function (ln) {
checkLine(ln, changeObj.origin);
});
// Is this a line which may have had a marker on it?
markerMgr.checkMarkers();
});
};
}());