2013-06-02 03:45:02 +04:00
|
|
|
// # Article Editor
|
|
|
|
|
2013-09-10 13:21:43 +04:00
|
|
|
/*global window, document, setTimeout, navigator, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable, JST */
|
2013-06-02 03:45:02 +04:00
|
|
|
(function () {
|
|
|
|
"use strict";
|
|
|
|
|
2013-09-02 01:03:01 +04:00
|
|
|
/*jslint regexp: true, bitwise: true */
|
2013-06-02 03:45:02 +04:00
|
|
|
var PublishBar,
|
|
|
|
ActionsWidget,
|
2013-09-02 01:03:01 +04:00
|
|
|
UploadManager,
|
|
|
|
MarkerManager,
|
2013-06-02 03:45:02 +04:00
|
|
|
MarkdownShortcuts = [
|
|
|
|
{'key': 'Ctrl+B', 'style': 'bold'},
|
|
|
|
{'key': 'Meta+B', 'style': 'bold'},
|
|
|
|
{'key': 'Ctrl+I', 'style': 'italic'},
|
|
|
|
{'key': 'Meta+I', 'style': 'italic'},
|
|
|
|
{'key': 'Ctrl+Alt+U', 'style': 'strike'},
|
|
|
|
{'key': 'Ctrl+Shift+K', 'style': 'code'},
|
2013-08-19 18:10:53 +04:00
|
|
|
{'key': 'Meta+K', 'style': 'code'},
|
2013-06-11 01:52:24 +04:00
|
|
|
{'key': 'Ctrl+Alt+1', 'style': 'h1'},
|
|
|
|
{'key': 'Ctrl+Alt+2', 'style': 'h2'},
|
|
|
|
{'key': 'Ctrl+Alt+3', 'style': 'h3'},
|
|
|
|
{'key': 'Ctrl+Alt+4', 'style': 'h4'},
|
|
|
|
{'key': 'Ctrl+Alt+5', 'style': 'h5'},
|
|
|
|
{'key': 'Ctrl+Alt+6', 'style': 'h6'},
|
2013-06-02 03:45:02 +04:00
|
|
|
{'key': 'Ctrl+Shift+L', 'style': 'link'},
|
|
|
|
{'key': 'Ctrl+Shift+I', 'style': 'image'},
|
|
|
|
{'key': 'Ctrl+Q', 'style': 'blockquote'},
|
2013-08-13 14:53:49 +04:00
|
|
|
{'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
|
2013-07-18 17:02:54 +04:00
|
|
|
{'key': 'Ctrl+U', 'style': 'uppercase'},
|
|
|
|
{'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
|
|
|
|
{'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
|
|
|
|
{'key': 'Ctrl+Alt+W', 'style': 'selectword'},
|
2013-07-22 16:50:50 +04:00
|
|
|
{'key': 'Ctrl+L', 'style': 'list'},
|
|
|
|
{'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
|
2013-09-27 17:41:38 +04:00
|
|
|
{'key': 'Meta+Alt+C', 'style': 'copyHTML'},
|
|
|
|
{'key': 'Meta+Enter', 'style': 'newLine'},
|
|
|
|
{'key': 'Ctrl+Enter', 'style': 'newLine'}
|
2013-09-02 01:03:01 +04:00
|
|
|
],
|
|
|
|
imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
|
|
|
|
markerRegex = /\{<([\w\W]*?)>\}/;
|
|
|
|
/*jslint regexp: false, bitwise: false */
|
2013-06-02 03:45:02 +04:00
|
|
|
|
|
|
|
// The publish bar associated with a post, which has the TagWidget and
|
|
|
|
// Save button and options and such.
|
|
|
|
// ----------------------------------------
|
2013-06-04 16:38:45 +04:00
|
|
|
PublishBar = Ghost.View.extend({
|
2013-06-02 03:45:02 +04:00
|
|
|
|
|
|
|
initialize: function () {
|
2013-08-21 16:55:58 +04:00
|
|
|
this.addSubview(new Ghost.View.EditorTagWidget({el: this.$('#entry-tags'), model: this.model})).render();
|
2013-06-02 03:45:02 +04:00
|
|
|
this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
|
2013-09-13 21:29:08 +04:00
|
|
|
this.addSubview(new Ghost.View.PostSettings({el: $('#entry-controls'), model: this.model})).render();
|
2013-08-02 02:33:06 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
render: function () { return this; }
|
2013-06-02 03:45:02 +04:00
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
// The Publish, Queue, Publish Now buttons
|
|
|
|
// ----------------------------------------
|
2013-06-04 16:38:45 +04:00
|
|
|
ActionsWidget = Ghost.View.extend({
|
2013-06-02 03:45:02 +04:00
|
|
|
|
|
|
|
events: {
|
|
|
|
'click [data-set-status]': 'handleStatus',
|
2013-09-15 22:14:36 +04:00
|
|
|
'click .js-publish-button': 'handlePostButton'
|
2013-06-02 03:45:02 +04:00
|
|
|
},
|
|
|
|
|
2013-09-15 22:14:36 +04:00
|
|
|
statusMap: null,
|
|
|
|
|
|
|
|
createStatusMap: {
|
2013-08-22 06:31:59 +04:00
|
|
|
'draft': 'Save Draft',
|
2013-09-15 22:14:36 +04:00
|
|
|
'published': 'Publish Now'
|
|
|
|
},
|
|
|
|
|
|
|
|
updateStatusMap: {
|
|
|
|
'draft': 'Unpublish',
|
|
|
|
'published': 'Update Post'
|
2013-06-02 03:45:02 +04:00
|
|
|
},
|
|
|
|
|
2013-09-07 19:07:47 +04:00
|
|
|
notificationMap: {
|
2013-09-18 00:35:30 +04:00
|
|
|
'draft': 'Your post has been saved as a draft.',
|
|
|
|
'published': 'Your post has been published.'
|
2013-09-10 02:10:00 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
errorMap: {
|
2013-09-18 00:35:30 +04:00
|
|
|
'draft': 'Your post could not be saved as a draft.',
|
|
|
|
'published': 'Your post could not be published.'
|
2013-09-07 19:07:47 +04:00
|
|
|
},
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
initialize: function () {
|
2013-06-11 01:52:24 +04:00
|
|
|
var self = this;
|
|
|
|
// Toggle publish
|
|
|
|
shortcut.add("Ctrl+Alt+P", function () {
|
|
|
|
self.toggleStatus();
|
|
|
|
});
|
|
|
|
shortcut.add("Ctrl+S", function () {
|
|
|
|
self.updatePost();
|
|
|
|
});
|
|
|
|
shortcut.add("Meta+S", function () {
|
|
|
|
self.updatePost();
|
|
|
|
});
|
2013-06-02 03:45:02 +04:00
|
|
|
this.listenTo(this.model, 'change:status', this.render);
|
2013-09-17 13:26:28 +04:00
|
|
|
this.listenTo(this.model, 'change:id', function (m) {
|
2013-09-15 15:13:06 +04:00
|
|
|
Backbone.history.navigate('/editor/' + m.id + '/');
|
2013-06-02 03:45:02 +04:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-06-11 01:52:24 +04:00
|
|
|
toggleStatus: function () {
|
2013-08-20 22:52:44 +04:00
|
|
|
var self = this,
|
2013-08-18 22:45:53 +04:00
|
|
|
keys = Object.keys(this.statusMap),
|
2013-09-15 22:14:36 +04:00
|
|
|
model = self.model,
|
|
|
|
prevStatus = model.get('status'),
|
2013-08-18 22:45:53 +04:00
|
|
|
currentIndex = keys.indexOf(prevStatus),
|
2013-09-18 00:35:30 +04:00
|
|
|
newIndex,
|
|
|
|
status;
|
2013-06-11 01:52:24 +04:00
|
|
|
|
2013-09-15 22:14:36 +04:00
|
|
|
newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
|
2013-09-18 00:35:30 +04:00
|
|
|
status = keys[newIndex];
|
2013-06-11 01:52:24 +04:00
|
|
|
|
2013-09-18 00:35:30 +04:00
|
|
|
this.setActiveStatus(keys[newIndex], this.statusMap[status], prevStatus);
|
2013-06-11 01:52:24 +04:00
|
|
|
|
|
|
|
this.savePost({
|
|
|
|
status: keys[newIndex]
|
|
|
|
}).then(function () {
|
2013-09-18 05:45:36 +04:00
|
|
|
self.reportSaveSuccess(status);
|
2013-08-20 22:52:44 +04:00
|
|
|
}, function (xhr) {
|
2013-08-18 22:45:53 +04:00
|
|
|
// Show a notification about the error
|
2013-08-20 22:52:44 +04:00
|
|
|
self.reportSaveError(xhr, model, status);
|
2013-06-11 01:52:24 +04:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-09-15 22:14:36 +04:00
|
|
|
setActiveStatus: function (newStatus, displayText, currentStatus) {
|
|
|
|
var isPublishing = (newStatus === 'published' && currentStatus !== 'published'),
|
|
|
|
isUnpublishing = (newStatus === 'draft' && currentStatus === 'published'),
|
|
|
|
// Controls when background of button has the splitbutton-delete/button-delete classes applied
|
|
|
|
isImportantStatus = (isPublishing || isUnpublishing);
|
|
|
|
|
|
|
|
$('.js-publish-splitbutton')
|
|
|
|
.removeClass(isImportantStatus ? 'splitbutton-save' : 'splitbutton-delete')
|
|
|
|
.addClass(isImportantStatus ? 'splitbutton-delete' : 'splitbutton-save');
|
|
|
|
|
|
|
|
// Set the publish button's action and proper coloring
|
|
|
|
$('.js-publish-button')
|
|
|
|
.attr('data-status', newStatus)
|
|
|
|
.text(displayText)
|
|
|
|
.removeClass(isImportantStatus ? 'button-save' : 'button-delete')
|
|
|
|
.addClass(isImportantStatus ? 'button-delete' : 'button-save');
|
2013-08-22 06:31:59 +04:00
|
|
|
|
2013-09-10 18:32:02 +04:00
|
|
|
// Remove the animated popup arrow
|
2013-09-15 22:14:36 +04:00
|
|
|
$('.js-publish-splitbutton > a')
|
2013-09-10 18:32:02 +04:00
|
|
|
.removeClass('active');
|
|
|
|
|
2013-08-22 06:31:59 +04:00
|
|
|
// Set the active action in the popup
|
2013-09-15 22:14:36 +04:00
|
|
|
$('.js-publish-splitbutton .editor-options li')
|
2013-08-22 06:31:59 +04:00
|
|
|
.removeClass('active')
|
2013-09-15 22:14:36 +04:00
|
|
|
.filter(['li[data-set-status="', newStatus, '"]'].join(''))
|
2013-08-22 06:31:59 +04:00
|
|
|
.addClass('active');
|
|
|
|
},
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
handleStatus: function (e) {
|
2013-08-22 06:31:59 +04:00
|
|
|
if (e) { e.preventDefault(); }
|
2013-09-15 22:14:36 +04:00
|
|
|
var status = $(e.currentTarget).attr('data-set-status'),
|
|
|
|
currentStatus = this.model.get('status');
|
2013-08-22 06:31:59 +04:00
|
|
|
|
2013-09-15 22:14:36 +04:00
|
|
|
this.setActiveStatus(status, this.statusMap[status], currentStatus);
|
2013-08-22 06:31:59 +04:00
|
|
|
|
|
|
|
// Dismiss the popup menu
|
|
|
|
$('body').find('.overlay:visible').fadeOut();
|
|
|
|
},
|
|
|
|
|
2013-09-02 04:38:06 +04:00
|
|
|
handlePostButton: function (e) {
|
2013-09-15 22:14:36 +04:00
|
|
|
if (e) { e.preventDefault(); }
|
|
|
|
var status = $(e.currentTarget).attr('data-status');
|
2013-09-02 04:38:06 +04:00
|
|
|
|
|
|
|
this.updatePost(status);
|
|
|
|
},
|
|
|
|
|
|
|
|
updatePost: function (status) {
|
2013-08-20 22:52:44 +04:00
|
|
|
var self = this,
|
|
|
|
model = this.model,
|
2013-09-10 02:10:00 +04:00
|
|
|
prevStatus = model.get('status');
|
2013-06-02 03:45:02 +04:00
|
|
|
|
2013-09-02 04:38:06 +04:00
|
|
|
// Default to same status if not passed in
|
|
|
|
status = status || prevStatus;
|
|
|
|
|
2013-09-18 23:04:39 +04:00
|
|
|
model.trigger('willSave');
|
|
|
|
|
2013-08-20 22:52:44 +04:00
|
|
|
this.savePost({
|
2013-06-02 03:45:02 +04:00
|
|
|
status: status
|
|
|
|
}).then(function () {
|
2013-09-18 05:45:36 +04:00
|
|
|
self.reportSaveSuccess(status);
|
2013-09-15 22:14:36 +04:00
|
|
|
// Refresh publish button and all relevant controls with updated status.
|
|
|
|
self.render();
|
2013-08-20 22:52:44 +04:00
|
|
|
}, function (xhr) {
|
2013-09-15 22:14:36 +04:00
|
|
|
// Set the model status back to previous
|
|
|
|
model.set({ status: prevStatus });
|
|
|
|
// Set appropriate button status
|
|
|
|
self.setActiveStatus(status, self.statusMap[status], prevStatus);
|
2013-08-20 22:52:44 +04:00
|
|
|
// Show a notification about the error
|
2013-09-18 00:35:30 +04:00
|
|
|
self.reportSaveError(xhr, model, status);
|
2013-06-04 16:38:45 +04:00
|
|
|
});
|
2013-06-02 03:45:02 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
savePost: function (data) {
|
2013-07-07 22:41:05 +04:00
|
|
|
_.each(this.model.blacklist, function (item) {
|
|
|
|
this.model.unset(item);
|
|
|
|
}, this);
|
|
|
|
|
2013-06-04 16:38:45 +04:00
|
|
|
var saved = this.model.save(_.extend({
|
2013-06-02 03:45:02 +04:00
|
|
|
title: $('#entry-title').val(),
|
2013-09-02 01:03:01 +04:00
|
|
|
// TODO: The content_raw getter here isn't great, shouldn't rely on currentView.
|
|
|
|
markdown: Ghost.currentView.getEditorValue()
|
2013-06-02 03:45:02 +04:00
|
|
|
}, data));
|
2013-06-04 16:38:45 +04:00
|
|
|
|
|
|
|
// TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
|
|
|
|
// ourselves for more consistent promises.
|
|
|
|
if (saved) {
|
|
|
|
return saved;
|
|
|
|
}
|
|
|
|
return $.Deferred().reject();
|
2013-06-02 03:45:02 +04:00
|
|
|
},
|
|
|
|
|
2013-09-18 05:45:36 +04:00
|
|
|
reportSaveSuccess: function (status) {
|
|
|
|
Ghost.notifications.clearEverything();
|
|
|
|
Ghost.notifications.addItem({
|
|
|
|
type: 'success',
|
|
|
|
message: this.notificationMap[status],
|
|
|
|
status: 'passive'
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-08-18 22:45:53 +04:00
|
|
|
reportSaveError: function (response, model, status) {
|
2013-09-18 00:35:30 +04:00
|
|
|
var message = this.errorMap[status];
|
2013-08-18 22:45:53 +04:00
|
|
|
|
|
|
|
if (response) {
|
|
|
|
// Get message from response
|
2013-09-18 00:35:30 +04:00
|
|
|
message += " " + Ghost.Views.Utils.getRequestErrorMessage(response);
|
2013-08-18 22:45:53 +04:00
|
|
|
} else if (model.validationError) {
|
|
|
|
// Grab a validation error
|
2013-09-10 02:10:00 +04:00
|
|
|
message += " " + model.validationError;
|
2013-08-18 22:45:53 +04:00
|
|
|
}
|
|
|
|
|
2013-09-18 05:45:36 +04:00
|
|
|
Ghost.notifications.clearEverything();
|
2013-08-18 22:45:53 +04:00
|
|
|
Ghost.notifications.addItem({
|
|
|
|
type: 'error',
|
|
|
|
message: message,
|
|
|
|
status: 'passive'
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-09-15 22:14:36 +04:00
|
|
|
setStatusLabels: function (statusMap) {
|
|
|
|
_.each(statusMap, function (label, status) {
|
|
|
|
$('li[data-set-status="' + status + '"] > a').text(label);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
render: function () {
|
2013-09-10 18:32:02 +04:00
|
|
|
var status = this.model.get('status');
|
|
|
|
|
2013-09-15 22:14:36 +04:00
|
|
|
// Assume that we're creating a new post
|
|
|
|
if (status !== 'published') {
|
|
|
|
this.statusMap = this.createStatusMap;
|
|
|
|
} else {
|
|
|
|
this.statusMap = this.updateStatusMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Populate the publish menu with the appropriate verbiage
|
|
|
|
this.setStatusLabels(this.statusMap);
|
|
|
|
|
2013-09-10 18:32:02 +04:00
|
|
|
// Default the selected publish option to the current status of the post.
|
2013-09-15 22:14:36 +04:00
|
|
|
this.setActiveStatus(status, this.statusMap[status], status);
|
2013-06-02 03:45:02 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2013-09-02 01:03:01 +04:00
|
|
|
// The entire /editor page's route
|
2013-06-02 03:45:02 +04:00
|
|
|
// ----------------------------------------
|
2013-06-04 16:38:45 +04:00
|
|
|
Ghost.Views.Editor = Ghost.View.extend({
|
2013-06-02 03:45:02 +04:00
|
|
|
|
|
|
|
initialize: function () {
|
|
|
|
|
|
|
|
// Add the container view for the Publish Bar
|
|
|
|
this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();
|
|
|
|
|
2013-09-15 20:10:57 +04:00
|
|
|
this.$('#entry-title').val(this.model.get('title')).focus();
|
2013-09-27 00:06:52 +04:00
|
|
|
this.$('#entry-markdown').text(this.model.get('markdown'));
|
2013-06-02 03:45:02 +04:00
|
|
|
|
2013-10-09 22:11:29 +04:00
|
|
|
this.listenTo(this.model, 'change:title', this.renderTitle);
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
this.initMarkdown();
|
|
|
|
this.renderPreview();
|
|
|
|
|
2013-08-01 18:28:13 +04:00
|
|
|
$('.entry-content header, .entry-preview header').on('click', function () {
|
|
|
|
$('.entry-content, .entry-preview').removeClass('active');
|
|
|
|
$(this).closest('section').addClass('active');
|
|
|
|
});
|
|
|
|
|
|
|
|
$('.entry-title .icon-fullscreen').on('click', function (e) {
|
|
|
|
e.preventDefault();
|
|
|
|
$('body').toggleClass('fullscreen');
|
|
|
|
});
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);
|
|
|
|
|
2013-09-04 20:21:50 +04:00
|
|
|
this.$('.CodeMirror-scroll').scrollClass({target: '.entry-markdown', offset: 10});
|
|
|
|
this.$('.entry-preview-content').scrollClass({target: '.entry-preview', offset: 10});
|
2013-06-02 03:45:02 +04:00
|
|
|
|
|
|
|
|
|
|
|
// Zen writing mode shortcut
|
|
|
|
shortcut.add("Alt+Shift+Z", function () {
|
|
|
|
$('body').toggleClass('zen');
|
|
|
|
});
|
|
|
|
|
|
|
|
$('.entry-markdown header, .entry-preview header').click(function (e) {
|
|
|
|
$('.entry-markdown, .entry-preview').removeClass('active');
|
|
|
|
$(e.target).closest('section').addClass('active');
|
|
|
|
});
|
|
|
|
|
2013-10-26 00:19:51 +04:00
|
|
|
// Deactivate default drag/drop action
|
|
|
|
$(document).bind('drop dragover', function (e) {
|
|
|
|
e.preventDefault();
|
|
|
|
});
|
2013-06-02 03:45:02 +04:00
|
|
|
},
|
|
|
|
|
2013-07-24 14:29:20 +04:00
|
|
|
events: {
|
2013-08-30 08:18:55 +04:00
|
|
|
'click .markdown-help': 'showHelp',
|
2013-09-10 13:21:43 +04:00
|
|
|
'blur #entry-title': 'trimTitle',
|
|
|
|
'orientationchange': 'orientationChange'
|
2013-07-24 14:29:20 +04:00
|
|
|
},
|
|
|
|
|
2013-09-18 05:09:21 +04:00
|
|
|
syncScroll: _.throttle(function (e) {
|
2013-06-02 03:45:02 +04:00
|
|
|
var $codeViewport = $(e.target),
|
|
|
|
$previewViewport = $('.entry-preview-content'),
|
|
|
|
$codeContent = $('.CodeMirror-sizer'),
|
|
|
|
$previewContent = $('.rendered-markdown'),
|
|
|
|
|
|
|
|
// calc position
|
|
|
|
codeHeight = $codeContent.height() - $codeViewport.height(),
|
|
|
|
previewHeight = $previewContent.height() - $previewViewport.height(),
|
|
|
|
ratio = previewHeight / codeHeight,
|
|
|
|
previewPostition = $codeViewport.scrollTop() * ratio;
|
|
|
|
|
|
|
|
// apply new scroll
|
|
|
|
$previewViewport.scrollTop(previewPostition);
|
2013-09-18 05:09:21 +04:00
|
|
|
}, 10),
|
2013-06-02 03:45:02 +04:00
|
|
|
|
2013-07-24 14:29:20 +04:00
|
|
|
showHelp: function () {
|
|
|
|
this.addSubview(new Ghost.Views.Modal({
|
|
|
|
model: {
|
2013-07-26 18:32:44 +04:00
|
|
|
options: {
|
|
|
|
close: true,
|
|
|
|
type: "info",
|
2013-09-06 18:36:16 +04:00
|
|
|
style: ["wide"],
|
2013-08-28 17:23:36 +04:00
|
|
|
animation: 'fade'
|
2013-07-24 14:29:20 +04:00
|
|
|
},
|
2013-07-26 18:32:44 +04:00
|
|
|
content: {
|
|
|
|
template: 'markdown',
|
|
|
|
title: 'Markdown Help'
|
|
|
|
}
|
2013-07-24 14:29:20 +04:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
|
2013-08-30 08:18:55 +04:00
|
|
|
trimTitle: function () {
|
|
|
|
var $title = $('#entry-title'),
|
|
|
|
rawTitle = $title.val(),
|
|
|
|
trimmedTitle = $.trim(rawTitle);
|
|
|
|
|
|
|
|
if (rawTitle !== trimmedTitle) {
|
|
|
|
$title.val(trimmedTitle);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2013-10-09 22:11:29 +04:00
|
|
|
renderTitle: function () {
|
|
|
|
this.$('#entry-title').val(this.model.get('title'));
|
|
|
|
},
|
|
|
|
|
2013-09-10 13:21:43 +04:00
|
|
|
// This is a hack to remove iOS6 white space on orientation change bug
|
|
|
|
// See: http://cl.ly/RGx9
|
|
|
|
orientationChange: function () {
|
|
|
|
if (/iPhone/.test(navigator.userAgent) && !/Opera Mini/.test(navigator.userAgent)) {
|
|
|
|
var focusedElement = document.activeElement,
|
|
|
|
s = document.documentElement.style;
|
|
|
|
focusedElement.blur();
|
|
|
|
s.display = 'none';
|
|
|
|
setTimeout(function () { s.display = 'block'; focusedElement.focus(); }, 0);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
// This updates the editor preview panel.
|
|
|
|
// Currently gets called on every key press.
|
|
|
|
// Also trigger word count update
|
|
|
|
renderPreview: function () {
|
2013-08-20 22:52:44 +04:00
|
|
|
var self = this,
|
2013-06-02 03:45:02 +04:00
|
|
|
preview = document.getElementsByClassName('rendered-markdown')[0];
|
|
|
|
preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
|
2013-09-02 01:03:01 +04:00
|
|
|
|
|
|
|
this.initUploads();
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
Countable.once(preview, function (counter) {
|
2013-08-20 22:52:44 +04:00
|
|
|
self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
|
|
|
|
self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
|
|
|
|
self.$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
|
2013-06-02 03:45:02 +04:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// Markdown converter & markdown shortcut initialization.
|
|
|
|
initMarkdown: function () {
|
2013-08-20 22:52:44 +04:00
|
|
|
var self = this;
|
|
|
|
|
2013-08-20 01:52:50 +04:00
|
|
|
this.converter = new Showdown.converter({extensions: ['ghostdown', 'github']});
|
2013-06-02 03:45:02 +04:00
|
|
|
this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
|
2013-08-20 01:52:50 +04:00
|
|
|
mode: 'gfm',
|
2013-06-02 03:45:02 +04:00
|
|
|
tabMode: 'indent',
|
2013-06-23 14:49:30 +04:00
|
|
|
tabindex: "2",
|
2013-08-19 23:23:44 +04:00
|
|
|
lineWrapping: true,
|
2013-10-17 01:46:13 +04:00
|
|
|
dragDrop: false,
|
|
|
|
extraKeys: {
|
|
|
|
Home: "goLineLeft",
|
|
|
|
End: "goLineRight"
|
|
|
|
}
|
2013-06-02 03:45:02 +04:00
|
|
|
});
|
2013-09-02 01:03:01 +04:00
|
|
|
this.uploadMgr = new UploadManager(this.editor);
|
2013-06-02 03:45:02 +04:00
|
|
|
|
2013-07-25 19:00:41 +04:00
|
|
|
// Inject modal for HTML to be viewed in
|
|
|
|
shortcut.add("Ctrl+Alt+C", function () {
|
2013-08-20 22:52:44 +04:00
|
|
|
self.showHTML();
|
2013-07-25 19:00:41 +04:00
|
|
|
});
|
|
|
|
shortcut.add("Ctrl+Alt+C", function () {
|
2013-08-20 22:52:44 +04:00
|
|
|
self.showHTML();
|
2013-07-25 19:00:41 +04:00
|
|
|
});
|
|
|
|
|
2013-06-02 03:45:02 +04:00
|
|
|
_.each(MarkdownShortcuts, function (combo) {
|
|
|
|
shortcut.add(combo.key, function () {
|
2013-08-20 22:52:44 +04:00
|
|
|
return self.editor.addMarkdown({style: combo.style});
|
2013-06-02 03:45:02 +04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2013-09-02 01:03:01 +04:00
|
|
|
this.enableEditor();
|
|
|
|
},
|
|
|
|
|
|
|
|
options: {
|
|
|
|
markers: {}
|
|
|
|
},
|
|
|
|
|
|
|
|
getEditorValue: function () {
|
|
|
|
return this.uploadMgr.getEditorValue();
|
|
|
|
},
|
|
|
|
|
|
|
|
initUploads: function () {
|
2013-10-02 13:39:34 +04:00
|
|
|
var filestorage = $('#entry-markdown-content').data('filestorage');
|
|
|
|
this.$('.js-drop-zone').upload({editor: true, fileStorage: filestorage});
|
2013-09-02 01:03:01 +04:00
|
|
|
this.$('.js-drop-zone').on('uploadstart', $.proxy(this.disableEditor, this));
|
|
|
|
this.$('.js-drop-zone').on('uploadfailure', $.proxy(this.enableEditor, this));
|
|
|
|
this.$('.js-drop-zone').on('uploadsuccess', $.proxy(this.enableEditor, this));
|
2013-10-11 00:37:09 +04:00
|
|
|
this.$('.js-drop-zone').on('uploadsuccess', this.uploadMgr.handleUpload);
|
2013-09-02 01:03:01 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
enableEditor: function () {
|
|
|
|
var self = this;
|
|
|
|
this.editor.setOption("readOnly", false);
|
2013-06-02 03:45:02 +04:00
|
|
|
this.editor.on('change', function () {
|
2013-08-20 22:52:44 +04:00
|
|
|
self.renderPreview();
|
2013-06-02 03:45:02 +04:00
|
|
|
});
|
2013-07-25 19:00:41 +04:00
|
|
|
},
|
|
|
|
|
2013-09-02 01:03:01 +04:00
|
|
|
disableEditor: function () {
|
|
|
|
var self = this;
|
|
|
|
this.editor.setOption("readOnly", "nocursor");
|
|
|
|
this.editor.off('change', function () {
|
|
|
|
self.renderPreview();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2013-07-25 19:00:41 +04:00
|
|
|
showHTML: function () {
|
|
|
|
this.addSubview(new Ghost.Views.Modal({
|
|
|
|
model: {
|
2013-07-26 18:32:44 +04:00
|
|
|
options: {
|
|
|
|
close: true,
|
|
|
|
type: "info",
|
2013-09-06 18:36:16 +04:00
|
|
|
style: ["wide"],
|
2013-08-28 17:23:36 +04:00
|
|
|
animation: 'fade'
|
2013-07-25 19:00:41 +04:00
|
|
|
},
|
2013-07-26 18:32:44 +04:00
|
|
|
content: {
|
|
|
|
template: 'copyToHTML',
|
|
|
|
title: 'Copied HTML'
|
|
|
|
}
|
2013-07-25 19:00:41 +04:00
|
|
|
}
|
|
|
|
}));
|
2013-08-02 02:33:06 +04:00
|
|
|
},
|
2013-06-27 07:52:56 +04:00
|
|
|
|
2013-08-02 02:33:06 +04:00
|
|
|
render: function () { return this; }
|
|
|
|
});
|
2013-06-27 07:52:56 +04:00
|
|
|
|
2013-09-02 01:03:01 +04:00
|
|
|
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?
|
|
|
|
}
|
|
|
|
|
2013-10-11 00:37:09 +04:00
|
|
|
function handleUpload(e, result_src) {
|
2013-09-02 01:03:01 +04:00
|
|
|
/*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 }
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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,
|
2013-10-11 00:37:09 +04:00
|
|
|
handleUpload: handleUpload
|
2013-09-02 01:03:01 +04:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2013-09-15 15:13:06 +04:00
|
|
|
}());
|