diff --git a/core/client/components/gh-codemirror.js b/core/client/components/gh-codemirror.js index d1eb1eccc0..6614f4083c 100644 --- a/core/client/components/gh-codemirror.js +++ b/core/client/components/gh-codemirror.js @@ -2,6 +2,7 @@ import MarkerManager from 'ghost/mixins/marker-manager'; import setScrollClassName from 'ghost/utils/set-scroll-classname'; +import 'ghost/utils/codemirror-shortcuts'; var onChangeHandler = function (cm, changeObj) { var line, diff --git a/core/client/controllers/editor/edit.js b/core/client/controllers/editor/edit.js index d0bcba2fce..49d36f4a03 100644 --- a/core/client/controllers/editor/edit.js +++ b/core/client/controllers/editor/edit.js @@ -1,16 +1,5 @@ import EditorControllerMixin from 'ghost/mixins/editor-base-controller'; -import MarkerManager from 'ghost/mixins/marker-manager'; -var EditorEditController = Ember.ObjectController.extend(EditorControllerMixin, MarkerManager, { - init: function () { - var self = this; - - this._super(); - - window.onbeforeunload = function () { - return self.get('isDirty') ? self.unloadDirtyMessage() : null; - }; - } -}); +var EditorEditController = Ember.ObjectController.extend(EditorControllerMixin); export default EditorEditController; diff --git a/core/client/controllers/editor/new.js b/core/client/controllers/editor/new.js index b5d5d32c47..ff9af9d876 100644 --- a/core/client/controllers/editor/new.js +++ b/core/client/controllers/editor/new.js @@ -1,17 +1,6 @@ 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: { /** * Redirect to editor after the first save diff --git a/core/client/mixins/editor-base-controller.js b/core/client/mixins/editor-base-controller.js index f6d664ae98..1e8552e6c2 100644 --- a/core/client/mixins/editor-base-controller.js +++ b/core/client/mixins/editor-base-controller.js @@ -15,6 +15,15 @@ Ember.get(PostModel, 'attributes').forEach(function (name) { watchedProps.push('tags.[]'); 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. * Only with a user-set value (via setSaveType action) diff --git a/core/client/mixins/editor-route-base.js b/core/client/mixins/editor-route-base.js index ee3ae5fa6d..bbf78bc3bc 100644 --- a/core/client/mixins/editor-route-base.js +++ b/core/client/mixins/editor-route-base.js @@ -1,12 +1,8 @@ import ShortcutsRoute from 'ghost/mixins/shortcuts-route'; import styleBody from 'ghost/mixins/style-body'; + var EditorRouteBase = Ember.Mixin.create(styleBody, ShortcutsRoute, { - shortcuts: { - 'ctrl+s, command+s': 'save', - 'ctrl+alt+p': 'publish', - 'ctrl+alt+z': 'toggleZenMode' - }, actions: { save: function () { this.get('controller').send('save'); @@ -18,8 +14,44 @@ var EditorRouteBase = Ember.Mixin.create(styleBody, ShortcutsRoute, { }, toggleZenMode: function () { 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; \ No newline at end of file diff --git a/core/client/mixins/shortcuts-route.js b/core/client/mixins/shortcuts-route.js index 21b258f2f7..bc9d21d1d3 100644 --- a/core/client/mixins/shortcuts-route.js +++ b/core/client/mixins/shortcuts-route.js @@ -14,12 +14,20 @@ key.filter = function () { * * To implement shortcuts, add this mixin to your `extend()`, * and implement a `shortcuts` hash. - * In this hash, keys are shortcut combinations - * (see [keymaster docs](https://github.com/madrobby/keymaster/blob/master/README.markdown)), and values are controller action names. + * 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)), + * * ```javascript * shortcuts: { * '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) { 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 event.preventDefault(); - //map the shortcut to its action - self.send(shortcuts[shortcut], event); + self.send(action, options); }); }); }, diff --git a/core/client/utils/codemirror-shortcuts.js b/core/client/utils/codemirror-shortcuts.js new file mode 100644 index 0000000000..6e1d932607 --- /dev/null +++ b/core/client/utils/codemirror-shortcuts.js @@ -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: '![$1](http://)', + 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) + }); + } + } +}; \ No newline at end of file