mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 05:14:12 +03:00
Merge pull request #758 from ErisDS/uploads-in-editor
Save image uploads in the editor
This commit is contained in:
commit
1870d00eef
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>';
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}());
|
Loading…
Reference in New Issue
Block a user