mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Merge pull request #3022 from novaugust/codemirror-shortcuts
Implement Markdown Shortcuts
This commit is contained in:
commit
42c9a272a7
7 changed files with 200 additions and 35 deletions
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import MarkerManager from 'ghost/mixins/marker-manager';
|
import MarkerManager from 'ghost/mixins/marker-manager';
|
||||||
import setScrollClassName from 'ghost/utils/set-scroll-classname';
|
import setScrollClassName from 'ghost/utils/set-scroll-classname';
|
||||||
|
import 'ghost/utils/codemirror-shortcuts';
|
||||||
|
|
||||||
var onChangeHandler = function (cm, changeObj) {
|
var onChangeHandler = function (cm, changeObj) {
|
||||||
var line,
|
var line,
|
||||||
|
|
|
@ -1,16 +1,5 @@
|
||||||
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
|
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
|
||||||
import MarkerManager from 'ghost/mixins/marker-manager';
|
|
||||||
|
|
||||||
var EditorEditController = Ember.ObjectController.extend(EditorControllerMixin, MarkerManager, {
|
var EditorEditController = Ember.ObjectController.extend(EditorControllerMixin);
|
||||||
init: function () {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this._super();
|
|
||||||
|
|
||||||
window.onbeforeunload = function () {
|
|
||||||
return self.get('isDirty') ? self.unloadDirtyMessage() : null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default EditorEditController;
|
export default EditorEditController;
|
||||||
|
|
|
@ -1,17 +1,6 @@
|
||||||
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
|
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
|
||||||
import MarkerManager from 'ghost/mixins/marker-manager';
|
|
||||||
|
|
||||||
var EditorNewController = Ember.ObjectController.extend(EditorControllerMixin, MarkerManager, {
|
|
||||||
init: function () {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this._super();
|
|
||||||
|
|
||||||
window.onbeforeunload = function () {
|
|
||||||
return self.get('isDirty') ? self.unloadDirtyMessage() : null;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
|
var EditorNewController = Ember.ObjectController.extend(EditorControllerMixin, {
|
||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
* Redirect to editor after the first save
|
* Redirect to editor after the first save
|
||||||
|
|
|
@ -15,6 +15,15 @@ Ember.get(PostModel, 'attributes').forEach(function (name) {
|
||||||
watchedProps.push('tags.[]');
|
watchedProps.push('tags.[]');
|
||||||
|
|
||||||
var EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
|
var EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
|
||||||
|
init: function () {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
window.onbeforeunload = function () {
|
||||||
|
return self.get('isDirty') ? self.unloadDirtyMessage() : null;
|
||||||
|
};
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* By default, a post will not change its publish state.
|
* By default, a post will not change its publish state.
|
||||||
* Only with a user-set value (via setSaveType action)
|
* Only with a user-set value (via setSaveType action)
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
|
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
|
||||||
import styleBody from 'ghost/mixins/style-body';
|
import styleBody from 'ghost/mixins/style-body';
|
||||||
|
|
||||||
|
|
||||||
var EditorRouteBase = Ember.Mixin.create(styleBody, ShortcutsRoute, {
|
var EditorRouteBase = Ember.Mixin.create(styleBody, ShortcutsRoute, {
|
||||||
shortcuts: {
|
|
||||||
'ctrl+s, command+s': 'save',
|
|
||||||
'ctrl+alt+p': 'publish',
|
|
||||||
'ctrl+alt+z': 'toggleZenMode'
|
|
||||||
},
|
|
||||||
actions: {
|
actions: {
|
||||||
save: function () {
|
save: function () {
|
||||||
this.get('controller').send('save');
|
this.get('controller').send('save');
|
||||||
|
@ -18,8 +14,44 @@ var EditorRouteBase = Ember.Mixin.create(styleBody, ShortcutsRoute, {
|
||||||
},
|
},
|
||||||
toggleZenMode: function () {
|
toggleZenMode: function () {
|
||||||
Ember.$('body').toggleClass('zen');
|
Ember.$('body').toggleClass('zen');
|
||||||
|
},
|
||||||
|
//The actual functionality is implemented in utils/codemirror-shortcuts
|
||||||
|
codeMirrorShortcut: function (options) {
|
||||||
|
this.get('controller.codemirror').shortcut(options.type);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
shortcuts: {
|
||||||
|
//General Editor shortcuts
|
||||||
|
'ctrl+s, command+s': 'save',
|
||||||
|
'ctrl+alt+p': 'publish',
|
||||||
|
'ctrl+alt+z': 'toggleZenMode',
|
||||||
|
//CodeMirror Markdown Shortcuts
|
||||||
|
'ctrl+alt+u': {action: 'codeMirrorShortcut', options: {type: 'strike'}},
|
||||||
|
'ctrl+alt+1': {action: 'codeMirrorShortcut', options: {type: 'h1'}},
|
||||||
|
'ctrl+alt+2': {action: 'codeMirrorShortcut', options: {type: 'h2'}},
|
||||||
|
'ctrl+alt+3': {action: 'codeMirrorShortcut', options: {type: 'h3'}},
|
||||||
|
'ctrl+alt+4': {action: 'codeMirrorShortcut', options: {type: 'h4'}},
|
||||||
|
'ctrl+alt+5': {action: 'codeMirrorShortcut', options: {type: 'h5'}},
|
||||||
|
'ctrl+alt+6': {action: 'codeMirrorShortcut', options: {type: 'h6'}},
|
||||||
|
'ctrl+shift+i': {action: 'codeMirrorShortcut', options: {type: 'image'}},
|
||||||
|
'ctrl+q': {action: 'codeMirrorShortcut', options: {type: 'blockquote'}},
|
||||||
|
'ctrl+shift+1': {action: 'codeMirrorShortcut', options: {type: 'currentDate'}},
|
||||||
|
'ctrl+b, command+b': {action: 'codeMirrorShortcut', options: {type: 'bold'}},
|
||||||
|
'ctrl+i, command+i': {action: 'codeMirrorShortcut', options: {type: 'italic'}},
|
||||||
|
'ctrl+k, command+k': {action: 'codeMirrorShortcut', options: {type: 'link'}},
|
||||||
|
'ctrl+l': {action: 'codeMirrorShortcut', options: {type: 'list'}},
|
||||||
|
/** Currently broken CodeMirror Markdown shortcuts.
|
||||||
|
* some may be broken due to a conflict with CodeMirror commands
|
||||||
|
* (see http://codemirror.net/doc/manual.html#commands)
|
||||||
|
'ctrl+U': {action: 'codeMirrorShortcut', options: {type: 'uppercase'}},
|
||||||
|
'ctrl+shift+U': {action: 'codeMirrorShortcut', options: {type: 'lowercase'}},
|
||||||
|
'ctrl+alt+shift+U': {action: 'codeMirrorShortcut', options: {type: 'titlecase'}}
|
||||||
|
'ctrl+alt+W': {action: 'codeMirrorShortcut', options: {type: 'selectword'}},
|
||||||
|
'ctrl+alt+c': {action: 'codeMirrorShortcut', options: {type: 'copyHTML'}},
|
||||||
|
'ctrl+enter': {action: 'codeMirrorShortcut', options: {type: 'newLine'}},
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default EditorRouteBase;
|
export default EditorRouteBase;
|
|
@ -14,12 +14,20 @@ key.filter = function () {
|
||||||
*
|
*
|
||||||
* To implement shortcuts, add this mixin to your `extend()`,
|
* To implement shortcuts, add this mixin to your `extend()`,
|
||||||
* and implement a `shortcuts` hash.
|
* and implement a `shortcuts` hash.
|
||||||
* In this hash, keys are shortcut combinations
|
* In this hash, keys are shortcut combinations and values are route action names.
|
||||||
* (see [keymaster docs](https://github.com/madrobby/keymaster/blob/master/README.markdown)), and values are controller action names.
|
* (see [keymaster docs](https://github.com/madrobby/keymaster/blob/master/README.markdown)),
|
||||||
|
*
|
||||||
* ```javascript
|
* ```javascript
|
||||||
* shortcuts: {
|
* shortcuts: {
|
||||||
* 'ctrl+s, command+s': 'save',
|
* 'ctrl+s, command+s': 'save',
|
||||||
* 'ctrl+alt+p': 'toggleZenMode'
|
* 'ctrl+alt+z': 'toggleZenMode'
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* For more complex actions, shortcuts can instead have their value
|
||||||
|
* be an object like {action, options}
|
||||||
|
* ```javascript
|
||||||
|
* shortcuts: {
|
||||||
|
* 'ctrl+k': {action: 'markdownShortcut', options: 'createLink'}
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
@ -30,10 +38,16 @@ var ShortcutsRoute = Ember.Mixin.create({
|
||||||
|
|
||||||
Ember.keys(shortcuts).forEach(function (shortcut) {
|
Ember.keys(shortcuts).forEach(function (shortcut) {
|
||||||
key(shortcut, function (event) {
|
key(shortcut, function (event) {
|
||||||
|
var action = shortcuts[shortcut],
|
||||||
|
options;
|
||||||
|
if (Ember.typeOf(action) !== 'string') {
|
||||||
|
options = action.options;
|
||||||
|
action = action.action;
|
||||||
|
}
|
||||||
|
|
||||||
//stop things like ctrl+s from actually opening a save dialogue
|
//stop things like ctrl+s from actually opening a save dialogue
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
//map the shortcut to its action
|
self.send(action, options);
|
||||||
self.send(shortcuts[shortcut], event);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
131
ghost/admin/utils/codemirror-shortcuts.js
Normal file
131
ghost/admin/utils/codemirror-shortcuts.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
/* global CodeMirror, moment */
|
||||||
|
/** Set up a shortcut function to be called via router actions.
|
||||||
|
* See editor-route-base
|
||||||
|
*/
|
||||||
|
|
||||||
|
//Used for simple, noncomputational replace-and-go! shortcuts.
|
||||||
|
// See default case in shortcut function below.
|
||||||
|
CodeMirror.prototype.simpleShortcutSyntax = {
|
||||||
|
bold: '**$1**',
|
||||||
|
italic: '*$1*',
|
||||||
|
strike: '~~$1~~',
|
||||||
|
code: '`$1`',
|
||||||
|
link: '[$1](http://)',
|
||||||
|
image: '',
|
||||||
|
blockquote: '> $1'
|
||||||
|
};
|
||||||
|
CodeMirror.prototype.shortcut = function (type) {
|
||||||
|
var text = this.getSelection(),
|
||||||
|
cursor = this.getCursor(),
|
||||||
|
line = this.getLine(cursor.line),
|
||||||
|
fromLineStart = {line: cursor.line, ch: 0},
|
||||||
|
md, letterCount, textIndex, position;
|
||||||
|
switch (type) {
|
||||||
|
case 'h1':
|
||||||
|
this.replaceRange('# ' + line, fromLineStart);
|
||||||
|
this.setCursor(cursor.line, cursor.ch + 2);
|
||||||
|
return;
|
||||||
|
case 'h2':
|
||||||
|
this.replaceRange('## ' + line, fromLineStart);
|
||||||
|
this.setCursor(cursor.line, cursor.ch + 3);
|
||||||
|
return;
|
||||||
|
case 'h3':
|
||||||
|
this.replaceRange('### ' + line, fromLineStart);
|
||||||
|
this.setCursor(cursor.line, cursor.ch + 4);
|
||||||
|
return;
|
||||||
|
case 'h4':
|
||||||
|
this.replaceRange('#### ' + line, fromLineStart);
|
||||||
|
this.setCursor(cursor.line, cursor.ch + 5);
|
||||||
|
return;
|
||||||
|
case 'h5':
|
||||||
|
this.replaceRange('##### ' + line, fromLineStart);
|
||||||
|
this.setCursor(cursor.line, cursor.ch + 6);
|
||||||
|
return;
|
||||||
|
case 'h6':
|
||||||
|
this.replaceRange('###### ' + line, fromLineStart);
|
||||||
|
this.setCursor(cursor.line, cursor.ch + 7);
|
||||||
|
return;
|
||||||
|
case 'link':
|
||||||
|
md = this.simpleShortcutSyntax.link.replace('$1', text);
|
||||||
|
this.replaceSelection(md, 'end');
|
||||||
|
if (!text) {
|
||||||
|
this.setCursor(cursor.line, cursor.ch + 1);
|
||||||
|
} else {
|
||||||
|
textIndex = line.indexOf(text, cursor.ch - text.length);
|
||||||
|
position = textIndex + md.length - 1;
|
||||||
|
this.setSelection({
|
||||||
|
line: cursor.line,
|
||||||
|
ch: position - 7
|
||||||
|
}, {
|
||||||
|
line: cursor.line,
|
||||||
|
ch: position
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'image':
|
||||||
|
md = this.simpleShortcutSyntax.image.replace('$1', text);
|
||||||
|
if (line !== '') {
|
||||||
|
md = '\n\n' + md;
|
||||||
|
}
|
||||||
|
this.replaceSelection(md, 'end');
|
||||||
|
cursor = this.getCursor();
|
||||||
|
this.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1});
|
||||||
|
return;
|
||||||
|
case 'list':
|
||||||
|
md = text.replace(/^(\s*)(\w\W*)/gm, '$1* $2');
|
||||||
|
this.replaceSelection(md, 'end');
|
||||||
|
return;
|
||||||
|
case 'currentDate':
|
||||||
|
md = moment(new Date()).format('D MMMM YYYY');
|
||||||
|
this.replaceSelection(md, 'end');
|
||||||
|
return;
|
||||||
|
/** @TODO
|
||||||
|
case 'uppercase':
|
||||||
|
md = text.toLocaleUpperCase();
|
||||||
|
break;
|
||||||
|
case 'lowercase':
|
||||||
|
md = text.toLocaleLowerCase();
|
||||||
|
break;
|
||||||
|
case 'titlecase':
|
||||||
|
md = text.toTitleCase();
|
||||||
|
break;
|
||||||
|
case 'selectword':
|
||||||
|
word = this.getTokenAt(cursor);
|
||||||
|
if (!/\w$/g.test(word.string)) {
|
||||||
|
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end - 1});
|
||||||
|
} else {
|
||||||
|
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'copyHTML':
|
||||||
|
converter = new Showdown.converter();
|
||||||
|
if (text) {
|
||||||
|
md = converter.makeHtml(text);
|
||||||
|
} else {
|
||||||
|
md = converter.makeHtml(this.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".modal-copyToHTML-content").text(md).selectText();
|
||||||
|
break;
|
||||||
|
case 'newLine':
|
||||||
|
if (line !== "") {
|
||||||
|
this.replaceRange(line + "\n\n", fromLineStart);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
*/
|
||||||
|
default:
|
||||||
|
if (this.simpleShortcutSyntax[type]) {
|
||||||
|
md = this.simpleShortcutSyntax[type].replace('$1', text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (md) {
|
||||||
|
this.replaceSelection(md, 'end');
|
||||||
|
if (!text) {
|
||||||
|
letterCount = md.length;
|
||||||
|
this.setCursor({
|
||||||
|
line: cursor.line,
|
||||||
|
ch: cursor.ch + (letterCount / 2)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue