Ghost/ghost/admin/views/editor.js
Matthew Harrison-Jones f6f095b381 Additional Keyboard Shortcuts and improvements to text highlighting
This fixes the event where text would be selected after manipulation from shortcut, the cursor is now placed after the text. On links and images the url field text is highlighted.
Additional shortcuts;

* Ctrl+U: Make text uppercase
* Ctrl+Shift+U: Make text lowercase
* Ctrl+Alt+Shift+U: Make text titlecase
* Ctrl+Alt+W: Select word
* Ctrl+L: Make into list
2013-07-18 14:02:54 +01:00

262 lines
9.4 KiB
JavaScript

// # Article Editor
/*global window, document, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable */
(function () {
"use strict";
var PublishBar,
TagWidget,
ActionsWidget,
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'},
{'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'},
{'key': 'Ctrl+Shift+L', 'style': 'link'},
{'key': 'Ctrl+Shift+I', 'style': 'image'},
{'key': 'Ctrl+Q', 'style': 'blockquote'},
{'key': 'Ctrl+Shift+1', 'style': 'currentdate'},
{'key': 'Ctrl+U', 'style': 'uppercase'},
{'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
{'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
{'key': 'Ctrl+Alt+W', 'style': 'selectword'},
{'key': 'Ctrl+L', 'style': 'list'}
];
// The publish bar associated with a post, which has the TagWidget and
// Save button and options and such.
// ----------------------------------------
PublishBar = Ghost.View.extend({
initialize: function () {
this.addSubview(new TagWidget({el: this.$('#entry-categories'), model: this.model})).render();
this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
}
});
// The Tag UI area associated with a post
// ----------------------------------------
TagWidget = Ghost.View.extend({
});
// The Publish, Queue, Publish Now buttons
// ----------------------------------------
ActionsWidget = Ghost.View.extend({
events: {
'click [data-set-status]': 'handleStatus',
'click .js-post-button': 'updatePost'
},
statusMap: {
'draft' : 'Save Draft',
'published': 'Update Post',
'scheduled' : 'Save Schedued Post'
},
initialize: function () {
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();
});
this.listenTo(this.model, 'change:status', this.render);
this.model.on('change:id', function (m) {
Backbone.history.navigate('/editor/' + m.id);
});
},
toggleStatus: function () {
var keys = Object.keys(this.statusMap),
model = this.model,
currentIndex = keys.indexOf(model.get('status')),
newIndex;
if (keys[currentIndex + 1] === 'scheduled') { // TODO: Remove once scheduled posts work
newIndex = currentIndex + 2 > keys.length - 1 ? 0 : currentIndex + 1;
} else {
newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
}
this.savePost({
status: keys[newIndex]
}).then(function () {
window.alert('Your post: ' + model.get('title') + ' has been ' + keys[newIndex]);
});
},
handleStatus: function (e) {
e.preventDefault();
var status = $(e.currentTarget).attr('data-set-status'),
model = this.model;
if (status === 'publish-on') {
return window.alert('Scheduled publishing not supported yet.');
}
if (status === 'queue') {
return window.alert('Scheduled publishing not supported yet.');
}
this.savePost({
status: status
}).then(function () {
window.alert('Your post: ' + model.get('title') + ' has been ' + status);
});
},
updatePost: function (e) {
if (e) {
e.preventDefault();
}
var model = this.model;
this.savePost().then(function () {
window.alert('Your post was saved as ' + model.get('status'));
}, function () {
window.alert(model.validationError);
});
},
savePost: function (data) {
// TODO: The content_raw getter here isn't great, shouldn't rely on currentView.
_.each(this.model.blacklist, function (item) {
this.model.unset(item);
}, this);
var saved = this.model.save(_.extend({
title: $('#entry-title').val(),
content_raw: Ghost.currentView.editor.getValue()
}, data));
// 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();
},
render: function () {
this.$('.js-post-button').text(this.statusMap[this.model.get('status')]);
}
});
// The entire /editor page's route (TODO: move all views to client side templates)
// ----------------------------------------
Ghost.Views.Editor = Ghost.View.extend({
initialize: function () {
// Add the container view for the Publish Bar
this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();
this.$('#entry-markdown').html(this.model.get('content_raw'));
this.initMarkdown();
this.renderPreview();
// TODO: Debounce
this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);
// Shadow on Markdown if scrolled
this.$('.CodeMirror-scroll').on('scroll', function (e) {
if ($('.CodeMirror-scroll').scrollTop() > 10) {
$('.entry-markdown').addClass('scrolling');
} else {
$('.entry-markdown').removeClass('scrolling');
}
});
// Shadow on Preview if scrolled
this.$('.entry-preview-content').on('scroll', function (e) {
if ($('.entry-preview-content').scrollTop() > 10) {
$('.entry-preview').addClass('scrolling');
} else {
$('.entry-preview').removeClass('scrolling');
}
});
// 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');
});
},
syncScroll: function (e) {
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);
},
// This updates the editor preview panel.
// Currently gets called on every key press.
// Also trigger word count update
renderPreview: function () {
var view = this,
preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
Countable.once(preview, function (counter) {
view.$('.entry-word-count').text(counter.words + ' words');
view.$('.entry-character-count').text(counter.characters + ' characters');
view.$('.entry-paragraph-count').text(counter.paragraphs + ' paragraphs');
});
},
// Markdown converter & markdown shortcut initialization.
initMarkdown: function () {
this.converter = new Showdown.converter({extensions: ['ghostdown']});
this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'markdown',
tabMode: 'indent',
tabindex: "2",
lineWrapping: true
});
var view = this;
_.each(MarkdownShortcuts, function (combo) {
shortcut.add(combo.key, function () {
return view.editor.addMarkdown({style: combo.style});
});
});
this.editor.on('change', function () {
view.renderPreview();
});
}
});
}());