mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 11:22:19 +03:00
Isolate all markdown editor behaviour into gh-editor
component
no issue - move all existing markdown editor behaviour out of the editor controller and isolate it into a single component that can be swapped out - split the `register/remove` functions of the `shortcuts-route` out into a separate `shortcuts` mixin
This commit is contained in:
parent
9b522457ef
commit
0cbd7d5c68
@ -32,7 +32,7 @@ export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
|
|||||||
|
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
|
|
||||||
this.sendAction('setEditor', this);
|
this.attrs.setEditor(this);
|
||||||
|
|
||||||
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
|
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
|
||||||
},
|
},
|
||||||
@ -43,22 +43,6 @@ export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable editing in the textarea (used while an upload is in progress)
|
|
||||||
*/
|
|
||||||
disable() {
|
|
||||||
let textarea = this.get('element');
|
|
||||||
textarea.setAttribute('readonly', 'readonly');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reenable editing in the textarea
|
|
||||||
*/
|
|
||||||
enable() {
|
|
||||||
let textarea = this.get('element');
|
|
||||||
textarea.removeAttribute('readonly');
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
toggleCopyHTMLModal(generatedHTML) {
|
toggleCopyHTMLModal(generatedHTML) {
|
||||||
this.attrs.toggleCopyHTMLModal(generatedHTML);
|
this.attrs.toggleCopyHTMLModal(generatedHTML);
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
import ShortcutsMixin from 'ghost/mixins/shortcuts';
|
||||||
|
import imageManager from 'ghost/utils/ed-image-manager';
|
||||||
|
import editorShortcuts from 'ghost/utils/editor-shortcuts';
|
||||||
|
|
||||||
const {Component, computed, run} = Ember;
|
const {Component, computed, run} = Ember;
|
||||||
const {equal} = computed;
|
const {equal} = computed;
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend(ShortcutsMixin, {
|
||||||
tagName: 'section',
|
tagName: 'section',
|
||||||
classNames: ['gh-view'],
|
classNames: ['view-container', 'view-editor'],
|
||||||
|
|
||||||
|
activeTab: 'markdown',
|
||||||
|
editor: null,
|
||||||
|
editorDisabled: undefined,
|
||||||
|
editorScrollInfo: null, // updated when gh-ed-editor component scrolls
|
||||||
|
height: null, // updated when markdown is rendered
|
||||||
|
shouldFocusEditor: false,
|
||||||
showCopyHTMLModal: false,
|
showCopyHTMLModal: false,
|
||||||
copyHTMLModalContent: null,
|
copyHTMLModalContent: null,
|
||||||
|
|
||||||
// updated when gh-ed-editor component scrolls
|
shortcuts: editorShortcuts,
|
||||||
editorScrollInfo: null,
|
|
||||||
// updated when markdown is rendered
|
|
||||||
height: null,
|
|
||||||
activeTab: 'markdown',
|
|
||||||
|
|
||||||
markdownActive: equal('activeTab', 'markdown'),
|
markdownActive: equal('activeTab', 'markdown'),
|
||||||
previewActive: equal('activeTab', 'preview'),
|
previewActive: equal('activeTab', 'preview'),
|
||||||
@ -25,8 +30,7 @@ export default Component.extend({
|
|||||||
// stays in sync
|
// stays in sync
|
||||||
scrollPosition: computed('editorScrollInfo', 'height', function () {
|
scrollPosition: computed('editorScrollInfo', 'height', function () {
|
||||||
let scrollInfo = this.get('editorScrollInfo');
|
let scrollInfo = this.get('editorScrollInfo');
|
||||||
let $previewContent = this.get('$previewContent');
|
let {$previewContent, $previewViewPort} = this;
|
||||||
let $previewViewPort = this.get('$previewViewPort');
|
|
||||||
|
|
||||||
if (!scrollInfo || !$previewContent || !$previewViewPort) {
|
if (!scrollInfo || !$previewContent || !$previewViewPort) {
|
||||||
return 0;
|
return 0;
|
||||||
@ -41,21 +45,23 @@ export default Component.extend({
|
|||||||
return previewPosition;
|
return previewPosition;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
scheduleAfterRender() {
|
|
||||||
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.scheduleAfterRender();
|
this.registerShortcuts();
|
||||||
|
run.scheduleOnce('afterRender', this, this._cacheElements);
|
||||||
},
|
},
|
||||||
|
|
||||||
afterRenderEvent() {
|
willDestroyElement() {
|
||||||
let $previewViewPort = this.$('.js-entry-preview-content');
|
if (this.attrs.onTeardown) {
|
||||||
|
this.attrs.onTeardown();
|
||||||
|
}
|
||||||
|
this.removeShortcuts();
|
||||||
|
},
|
||||||
|
|
||||||
|
_cacheElements() {
|
||||||
// cache these elements for use in other methods
|
// cache these elements for use in other methods
|
||||||
this.set('$previewViewPort', $previewViewPort);
|
this.$previewViewPort = this.$('.js-entry-preview-content');
|
||||||
this.set('$previewContent', this.$('.js-rendered-markdown'));
|
this.$previewContent = this.$('.js-rendered-markdown');
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@ -63,6 +69,52 @@ export default Component.extend({
|
|||||||
this.set('activeTab', tab);
|
this.set('activeTab', tab);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateScrollInfo(scrollInfo) {
|
||||||
|
this.set('editorScrollInfo', scrollInfo);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHeight(height) {
|
||||||
|
this.set('height', height);
|
||||||
|
},
|
||||||
|
|
||||||
|
// set from a `sendAction` on the gh-ed-editor component,
|
||||||
|
// so that we get a reference for handling uploads.
|
||||||
|
setEditor(editor) {
|
||||||
|
this.set('editor', editor);
|
||||||
|
},
|
||||||
|
|
||||||
|
disableEditor() {
|
||||||
|
this.set('editorDisabled', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
enableEditor() {
|
||||||
|
this.set('editorDisabled', undefined);
|
||||||
|
},
|
||||||
|
|
||||||
|
// The actual functionality is implemented in utils/ed-editor-shortcuts
|
||||||
|
editorShortcut(options) {
|
||||||
|
if (this.editor.$().is(':focus')) {
|
||||||
|
this.editor.shortcut(options.type);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Match the uploaded file to a line in the editor, and update that line with a path reference
|
||||||
|
// ensuring that everything ends up in the correct place and format.
|
||||||
|
handleImgUpload(e, resultSrc) {
|
||||||
|
let editor = this.get('editor');
|
||||||
|
let editorValue = editor.getValue();
|
||||||
|
let replacement = imageManager.getSrcRange(editorValue, e.target);
|
||||||
|
let cursorPosition;
|
||||||
|
|
||||||
|
if (replacement) {
|
||||||
|
cursorPosition = replacement.start + resultSrc.length + 1;
|
||||||
|
if (replacement.needsParens) {
|
||||||
|
resultSrc = `(${resultSrc})`;
|
||||||
|
}
|
||||||
|
editor.replaceSelection(resultSrc, replacement.start, replacement.end, cursorPosition);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
toggleCopyHTMLModal(generatedHTML) {
|
toggleCopyHTMLModal(generatedHTML) {
|
||||||
this.set('copyHTMLModalContent', generatedHTML);
|
this.set('copyHTMLModalContent', generatedHTML);
|
||||||
this.toggleProperty('showCopyHTMLModal');
|
this.toggleProperty('showCopyHTMLModal');
|
||||||
|
@ -61,7 +61,7 @@ export default Mixin.create({
|
|||||||
*/
|
*/
|
||||||
scrollHandler() {
|
scrollHandler() {
|
||||||
this.set('scrollThrottle', run.throttle(this, () => {
|
this.set('scrollThrottle', run.throttle(this, () => {
|
||||||
this.sendAction('updateScrollInfo', this.getScrollInfo());
|
this.attrs.updateScrollInfo(this.getScrollInfo());
|
||||||
}, 10));
|
}, 10));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
import PostModel from 'ghost/models/post';
|
import PostModel from 'ghost/models/post';
|
||||||
import boundOneWay from 'ghost/utils/bound-one-way';
|
import boundOneWay from 'ghost/utils/bound-one-way';
|
||||||
import imageManager from 'ghost/utils/ed-image-manager';
|
|
||||||
|
|
||||||
const {Mixin, RSVP, computed, inject, observer, run} = Ember;
|
const {Mixin, RSVP, computed, inject, observer, run} = Ember;
|
||||||
const {alias} = computed;
|
const {alias} = computed;
|
||||||
@ -17,7 +16,6 @@ PostModel.eachAttribute(function (name) {
|
|||||||
export default Mixin.create({
|
export default Mixin.create({
|
||||||
_autoSaveId: null,
|
_autoSaveId: null,
|
||||||
_timedSaveId: null,
|
_timedSaveId: null,
|
||||||
editor: null,
|
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
|
||||||
showLeaveEditorModal: false,
|
showLeaveEditorModal: false,
|
||||||
@ -126,7 +124,7 @@ export default Mixin.create({
|
|||||||
let markdown = model.get('markdown');
|
let markdown = model.get('markdown');
|
||||||
let title = model.get('title');
|
let title = model.get('title');
|
||||||
let titleScratch = model.get('titleScratch');
|
let titleScratch = model.get('titleScratch');
|
||||||
let scratch = this.get('editor').getValue();
|
let scratch = this.get('model.scratch');
|
||||||
let changedAttributes;
|
let changedAttributes;
|
||||||
|
|
||||||
if (!this.tagNamesEqual()) {
|
if (!this.tagNamesEqual()) {
|
||||||
@ -253,31 +251,9 @@ export default Mixin.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save(options) {
|
cancelTimers() {
|
||||||
let prevStatus = this.get('model.status');
|
|
||||||
let isNew = this.get('model.isNew');
|
|
||||||
let autoSaveId = this._autoSaveId;
|
let autoSaveId = this._autoSaveId;
|
||||||
let timedSaveId = this._timedSaveId;
|
let timedSaveId = this._timedSaveId;
|
||||||
let psmController = this.get('postSettingsMenuController');
|
|
||||||
let promise, status;
|
|
||||||
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
// when navigating quickly between pages autoSave will occasionally
|
|
||||||
// try to run after the editor has been torn down so bail out here
|
|
||||||
// before we throw errors
|
|
||||||
if (!this.get('editor').$()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleProperty('submitting');
|
|
||||||
|
|
||||||
if (options.backgroundSave) {
|
|
||||||
// do not allow a post's status to be set to published by a background save
|
|
||||||
status = 'draft';
|
|
||||||
} else {
|
|
||||||
status = this.get('willPublish') ? 'published' : 'draft';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoSaveId) {
|
if (autoSaveId) {
|
||||||
run.cancel(autoSaveId);
|
run.cancel(autoSaveId);
|
||||||
@ -288,10 +264,30 @@ export default Mixin.create({
|
|||||||
run.cancel(timedSaveId);
|
run.cancel(timedSaveId);
|
||||||
this._timedSaveId = null;
|
this._timedSaveId = null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
save(options) {
|
||||||
|
let prevStatus = this.get('model.status');
|
||||||
|
let isNew = this.get('model.isNew');
|
||||||
|
let psmController = this.get('postSettingsMenuController');
|
||||||
|
let promise, status;
|
||||||
|
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
this.toggleProperty('submitting');
|
||||||
|
|
||||||
|
if (options.backgroundSave) {
|
||||||
|
// do not allow a post's status to be set to published by a background save
|
||||||
|
status = 'draft';
|
||||||
|
} else {
|
||||||
|
status = this.get('willPublish') ? 'published' : 'draft';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.send('cancelTimers');
|
||||||
|
|
||||||
// Set the properties that are indirected
|
// Set the properties that are indirected
|
||||||
// set markdown equal to what's in the editor, minus the image markers.
|
// set markdown equal to what's in the editor, minus the image markers.
|
||||||
this.set('model.markdown', this.get('editor').getValue());
|
this.set('model.markdown', this.get('model.scratch'));
|
||||||
this.set('model.status', status);
|
this.set('model.status', status);
|
||||||
|
|
||||||
// Set a default title
|
// Set a default title
|
||||||
@ -345,53 +341,12 @@ export default Mixin.create({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// set from a `sendAction` on the gh-ed-editor component,
|
|
||||||
// so that we get a reference for handling uploads.
|
|
||||||
setEditor(editor) {
|
|
||||||
this.set('editor', editor);
|
|
||||||
},
|
|
||||||
|
|
||||||
// fired from the gh-ed-preview component when an image upload starts
|
|
||||||
disableEditor() {
|
|
||||||
this.get('editor').disable();
|
|
||||||
},
|
|
||||||
|
|
||||||
// fired from the gh-ed-preview component when an image upload finishes
|
|
||||||
enableEditor() {
|
|
||||||
this.get('editor').enable();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Match the uploaded file to a line in the editor, and update that line with a path reference
|
|
||||||
// ensuring that everything ends up in the correct place and format.
|
|
||||||
handleImgUpload(e, resultSrc) {
|
|
||||||
let editor = this.get('editor');
|
|
||||||
let editorValue = editor.getValue();
|
|
||||||
let replacement = imageManager.getSrcRange(editorValue, e.target);
|
|
||||||
let cursorPosition;
|
|
||||||
|
|
||||||
if (replacement) {
|
|
||||||
cursorPosition = replacement.start + resultSrc.length + 1;
|
|
||||||
if (replacement.needsParens) {
|
|
||||||
resultSrc = `(${resultSrc})`;
|
|
||||||
}
|
|
||||||
editor.replaceSelection(resultSrc, replacement.start, replacement.end, cursorPosition);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
autoSaveNew() {
|
autoSaveNew() {
|
||||||
if (this.get('model.isNew')) {
|
if (this.get('model.isNew')) {
|
||||||
this.send('save', {silent: true, backgroundSave: true});
|
this.send('save', {silent: true, backgroundSave: true});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorScrollInfo(scrollInfo) {
|
|
||||||
this.set('editorScrollInfo', scrollInfo);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateHeight(height) {
|
|
||||||
this.set('height', height);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleLeaveEditorModal(transition) {
|
toggleLeaveEditorModal(transition) {
|
||||||
this.set('leaveEditorTransition', transition);
|
this.set('leaveEditorTransition', transition);
|
||||||
this.toggleProperty('showLeaveEditorModal');
|
this.toggleProperty('showLeaveEditorModal');
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
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';
|
||||||
import editorShortcuts from 'ghost/utils/editor-shortcuts';
|
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
|
||||||
|
|
||||||
const {Mixin, RSVP, run} = Ember;
|
const {Mixin, RSVP, run} = Ember;
|
||||||
|
|
||||||
|
let generalShortcuts = {};
|
||||||
|
generalShortcuts[`${ctrlOrCmd}+alt+p`] = 'publish';
|
||||||
|
generalShortcuts['alt+shift+z'] = 'toggleZenMode';
|
||||||
|
|
||||||
export default Mixin.create(styleBody, ShortcutsRoute, {
|
export default Mixin.create(styleBody, ShortcutsRoute, {
|
||||||
classNames: ['editor'],
|
classNames: ['editor'],
|
||||||
|
|
||||||
|
shortcuts: generalShortcuts,
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
save() {
|
save() {
|
||||||
this.get('controller').send('save');
|
this.get('controller').send('save');
|
||||||
@ -24,14 +30,6 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
|
|||||||
Ember.$('body').toggleClass('zen');
|
Ember.$('body').toggleClass('zen');
|
||||||
},
|
},
|
||||||
|
|
||||||
// The actual functionality is implemented in utils/ed-editor-shortcuts
|
|
||||||
editorShortcut(options) {
|
|
||||||
// Only fire editor shortcuts when the editor has focus.
|
|
||||||
if (this.get('controller.editor').$().is(':focus')) {
|
|
||||||
this.get('controller.editor').shortcut(options.type);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
willTransition(transition) {
|
willTransition(transition) {
|
||||||
let controller = this.get('controller');
|
let controller = this.get('controller');
|
||||||
let scratch = controller.get('model.scratch');
|
let scratch = controller.get('model.scratch');
|
||||||
@ -97,8 +95,6 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
shortcuts: editorShortcuts,
|
|
||||||
|
|
||||||
attachModelHooks(controller, model) {
|
attachModelHooks(controller, model) {
|
||||||
// this will allow us to track when the model is saved and update the controller
|
// this will allow us to track when the model is saved and update the controller
|
||||||
// so that we can be sure controller.hasDirtyAttributes is correct, without having to update the
|
// so that we can be sure controller.hasDirtyAttributes is correct, without having to update the
|
||||||
|
@ -1,16 +1,8 @@
|
|||||||
/* global key */
|
|
||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
|
import ShortcutsMixin from 'ghost/mixins/shortcuts';
|
||||||
|
|
||||||
const {Mixin, run, typeOf} = Ember;
|
const {Mixin} = Ember;
|
||||||
|
|
||||||
// Configure KeyMaster to respond to all shortcuts,
|
|
||||||
// even inside of
|
|
||||||
// input, textarea, and select.
|
|
||||||
key.filter = function () {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
key.setScope('default');
|
|
||||||
/**
|
/**
|
||||||
* Only routes can implement shortcuts.
|
* Only routes can implement shortcuts.
|
||||||
* If you need to trigger actions on the controller,
|
* If you need to trigger actions on the controller,
|
||||||
@ -44,39 +36,7 @@ key.setScope('default');
|
|||||||
* To have all your shortcut work in all scopes, give it the scope "all".
|
* To have all your shortcut work in all scopes, give it the scope "all".
|
||||||
* Find out more at the keymaster docs
|
* Find out more at the keymaster docs
|
||||||
*/
|
*/
|
||||||
export default Mixin.create({
|
export default Mixin.create(ShortcutsMixin, {
|
||||||
registerShortcuts() {
|
|
||||||
let shortcuts = this.get('shortcuts');
|
|
||||||
|
|
||||||
Object.keys(shortcuts).forEach((shortcut) => {
|
|
||||||
let scope = shortcuts[shortcut].scope || 'default';
|
|
||||||
let action = shortcuts[shortcut];
|
|
||||||
let options;
|
|
||||||
|
|
||||||
if (typeOf(action) !== 'string') {
|
|
||||||
options = action.options;
|
|
||||||
action = action.action;
|
|
||||||
}
|
|
||||||
|
|
||||||
key(shortcut, scope, (event) => {
|
|
||||||
// stop things like ctrl+s from actually opening a save dialogue
|
|
||||||
event.preventDefault();
|
|
||||||
run(this, function () {
|
|
||||||
this.send(action, options);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
removeShortcuts() {
|
|
||||||
let shortcuts = this.get('shortcuts');
|
|
||||||
|
|
||||||
Object.keys(shortcuts).forEach((shortcut) => {
|
|
||||||
let scope = shortcuts[shortcut].scope || 'default';
|
|
||||||
key.unbind(shortcut, scope);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
activate() {
|
activate() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.registerShortcuts();
|
this.registerShortcuts();
|
||||||
|
79
ghost/admin/app/mixins/shortcuts.js
Normal file
79
ghost/admin/app/mixins/shortcuts.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
/* global key */
|
||||||
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
const {Mixin, run, typeOf} = Ember;
|
||||||
|
|
||||||
|
// Configure KeyMaster to respond to all shortcuts,
|
||||||
|
// even inside of
|
||||||
|
// input, textarea, and select.
|
||||||
|
key.filter = function () {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
key.setScope('default');
|
||||||
|
/**
|
||||||
|
* Only routes can implement shortcuts.
|
||||||
|
* If you need to trigger actions on the controller,
|
||||||
|
* simply call them with `this.get('controller').send('action')`.
|
||||||
|
*
|
||||||
|
* To implement shortcuts, add this mixin to your `extend()`,
|
||||||
|
* and implement a `shortcuts` hash.
|
||||||
|
* 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+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'}
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* You can set the scope of your shortcut by passing a scope property.
|
||||||
|
* ```javascript
|
||||||
|
* shortcuts : {
|
||||||
|
* 'enter': {action : 'confirmModal', scope: 'modal'}
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* If you don't specify a scope, we use a default scope called "default".
|
||||||
|
* To have all your shortcut work in all scopes, give it the scope "all".
|
||||||
|
* Find out more at the keymaster docs
|
||||||
|
*/
|
||||||
|
export default Mixin.create({
|
||||||
|
registerShortcuts() {
|
||||||
|
let shortcuts = this.get('shortcuts');
|
||||||
|
|
||||||
|
Object.keys(shortcuts).forEach((shortcut) => {
|
||||||
|
let scope = shortcuts[shortcut].scope || 'default';
|
||||||
|
let action = shortcuts[shortcut];
|
||||||
|
let options;
|
||||||
|
|
||||||
|
if (typeOf(action) !== 'string') {
|
||||||
|
options = action.options;
|
||||||
|
action = action.action;
|
||||||
|
}
|
||||||
|
|
||||||
|
key(shortcut, scope, (event) => {
|
||||||
|
// stop things like ctrl+s from actually opening a save dialogue
|
||||||
|
event.preventDefault();
|
||||||
|
run(this, function () {
|
||||||
|
this.send(action, options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeShortcuts() {
|
||||||
|
let shortcuts = this.get('shortcuts');
|
||||||
|
|
||||||
|
Object.keys(shortcuts).forEach((shortcut) => {
|
||||||
|
let scope = shortcuts[shortcut].scope || 'default';
|
||||||
|
key.unbind(shortcut, scope);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -1,4 +1,46 @@
|
|||||||
{{yield this (action 'toggleCopyHTMLModal')}}
|
<section class="entry-markdown js-entry-markdown {{if markdownActive 'active'}}">
|
||||||
|
<header class="floatingheader">
|
||||||
|
<span class="desktop-tabs"><a class="markdown-help-label" href="" title="Markdown Help" {{action (route-action "toggleMarkdownHelpModal")}}>Markdown</a></span>
|
||||||
|
<span class="mobile-tabs">
|
||||||
|
<a href="#" {{action 'selectTab' 'markdown'}} class="{{if markdownActive 'active'}}">Markdown</a>
|
||||||
|
<a href="#" {{action 'selectTab' 'preview'}} class="{{if previewActive 'active'}}">Preview</a>
|
||||||
|
</span>
|
||||||
|
<a class="markdown-help-icon" href="" title="Markdown Help" {{action (route-action "toggleMarkdownHelpModal")}}><i class="icon-markdown"></i></a>
|
||||||
|
</header>
|
||||||
|
<section id="entry-markdown-content" class="entry-markdown-content">
|
||||||
|
{{gh-ed-editor classNames="markdown-editor js-markdown-editor"
|
||||||
|
tabindex="1"
|
||||||
|
spellcheck="true"
|
||||||
|
value=value
|
||||||
|
setEditor=(action "setEditor")
|
||||||
|
updateScrollInfo=(action "updateScrollInfo")
|
||||||
|
toggleCopyHTMLModal=(action "toggleCopyHTMLModal")
|
||||||
|
onFocusIn=editorFocused
|
||||||
|
height=height
|
||||||
|
focus=shouldFocusEditor
|
||||||
|
readonly=editorDisabled}}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="entry-preview js-entry-preview {{if previewActive 'active'}}">
|
||||||
|
<header class="floatingheader">
|
||||||
|
<span class="desktop-tabs"><a target="_blank" href="{{previewUrl}}">Preview</a></span>
|
||||||
|
<span class="mobile-tabs">
|
||||||
|
<a href="#" {{action 'selectTab' 'markdown'}} class="{{if markdownActive 'active'}}">Markdown</a>
|
||||||
|
<a href="#" {{action 'selectTab' 'preview'}} class="{{if previewActive 'active'}}">Preview</a>
|
||||||
|
</span>
|
||||||
|
<span class="entry-word-count">{{gh-count-words value}}</span>
|
||||||
|
</header>
|
||||||
|
<section class="entry-preview-content js-entry-preview-content">
|
||||||
|
{{gh-ed-preview classNames="rendered-markdown js-rendered-markdown"
|
||||||
|
markdown=value
|
||||||
|
scrollPosition=scrollPosition
|
||||||
|
updateHeight=(action "updateHeight")
|
||||||
|
uploadStarted=(action "disableEditor")
|
||||||
|
uploadFinished=(action "enableEditor")
|
||||||
|
uploadSuccess=(action "handleImgUpload")}}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
{{#if showCopyHTMLModal}}
|
{{#if showCopyHTMLModal}}
|
||||||
{{gh-fullscreen-modal "copy-html"
|
{{gh-fullscreen-modal "copy-html"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{{#gh-editor editorScrollInfo=editorScrollInfo as |ghEditor toggleCopyHTMLModal|}}
|
<section class="gh-view">
|
||||||
<header class="view-header">
|
<header class="view-header">
|
||||||
{{#gh-view-title classNames="gh-editor-title" openMobileMenu="openMobileMenu"}}
|
{{#gh-view-title classNames="gh-editor-title" openMobileMenu="openMobileMenu"}}
|
||||||
{{gh-trim-focus-input type="text" id="entry-title" placeholder="Your Post Title" value=model.titleScratch tabindex="1" focus=shouldFocusTitle}}
|
{{gh-trim-focus-input type="text" id="entry-title" placeholder="Your Post Title" value=model.titleScratch tabindex="1" focus=shouldFocusTitle}}
|
||||||
@ -20,47 +20,11 @@
|
|||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="view-container view-editor">
|
{{gh-editor value=model.scratch
|
||||||
<section class="entry-markdown js-entry-markdown {{if ghEditor.markdownActive 'active'}}">
|
shouldFocusEditor=shouldFocusEditor
|
||||||
<header class="floatingheader">
|
editorFocused=(action "autoSaveNew")
|
||||||
<span class="desktop-tabs"><a class="markdown-help-label" href="" title="Markdown Help" {{action "toggleMarkdownHelpModal"}}>Markdown</a></span>
|
onTeardown=(action "cancelTimers")}}
|
||||||
<span class="mobile-tabs">
|
</section>
|
||||||
<a href="#" {{action 'selectTab' 'markdown' target=ghEditor}} class="{{if ghEditor.markdownActive 'active'}}">Markdown</a>
|
|
||||||
<a href="#" {{action 'selectTab' 'preview' target=ghEditor}} class="{{if ghEditor.previewActive 'active'}}">Preview</a>
|
|
||||||
</span>
|
|
||||||
<a class="markdown-help-icon" href="" title="Markdown Help" {{action "toggleMarkdownHelpModal"}}><i class="icon-markdown"></i></a>
|
|
||||||
</header>
|
|
||||||
<section id="entry-markdown-content" class="entry-markdown-content">
|
|
||||||
{{gh-ed-editor classNames="markdown-editor js-markdown-editor"
|
|
||||||
tabindex="1"
|
|
||||||
spellcheck="true"
|
|
||||||
value=model.scratch
|
|
||||||
setEditor="setEditor"
|
|
||||||
updateScrollInfo="updateEditorScrollInfo"
|
|
||||||
toggleCopyHTMLModal=toggleCopyHTMLModal
|
|
||||||
onFocusIn="autoSaveNew"
|
|
||||||
height=height
|
|
||||||
focus=shouldFocusEditor}}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="entry-preview js-entry-preview {{if ghEditor.previewActive 'active'}}">
|
|
||||||
<header class="floatingheader">
|
|
||||||
<span class="desktop-tabs"><a target="_blank" href="{{model.previewUrl}}">Preview</a></span>
|
|
||||||
<span class="mobile-tabs">
|
|
||||||
<a href="#" {{action 'selectTab' 'markdown' target=ghEditor}} class="{{if ghEditor.markdownActive 'active'}}">Markdown</a>
|
|
||||||
<a href="#" {{action 'selectTab' 'preview' target=ghEditor}} class="{{if ghEditor.previewActive 'active'}}">Preview</a>
|
|
||||||
</span>
|
|
||||||
<span class="entry-word-count">{{gh-count-words model.scratch}}</span>
|
|
||||||
</header>
|
|
||||||
<section class="entry-preview-content js-entry-preview-content">
|
|
||||||
{{gh-ed-preview classNames="rendered-markdown js-rendered-markdown"
|
|
||||||
markdown=model.scratch scrollPosition=ghEditor.scrollPosition updateHeight="updateHeight"
|
|
||||||
uploadStarted="disableEditor" uploadFinished="enableEditor" uploadSuccess="handleImgUpload"}}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
{{/gh-editor}}
|
|
||||||
|
|
||||||
{{#if showDeletePostModal}}
|
{{#if showDeletePostModal}}
|
||||||
{{gh-fullscreen-modal "delete-post"
|
{{gh-fullscreen-modal "delete-post"
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
// # Editor shortcuts
|
// # Editor shortcuts
|
||||||
// Loaded by EditorBaseRoute, which is a shortcuts route
|
// Loaded by gh-editor component
|
||||||
// This map is used to ensure the right action is called by each shortcut
|
// This map is used to ensure the right action is called by each shortcut
|
||||||
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
|
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
|
||||||
|
|
||||||
let shortcuts = {};
|
let shortcuts = {};
|
||||||
|
|
||||||
// General editor shortcuts
|
|
||||||
shortcuts[`${ctrlOrCmd}+alt+p`] = 'publish';
|
|
||||||
shortcuts['alt+shift+z'] = 'toggleZenMode';
|
|
||||||
|
|
||||||
// Markdown Shortcuts
|
// Markdown Shortcuts
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
|
@ -9,9 +9,15 @@ describeComponent(
|
|||||||
'gh-editor',
|
'gh-editor',
|
||||||
'Unit: Component: gh-editor',
|
'Unit: Component: gh-editor',
|
||||||
{
|
{
|
||||||
unit: true
|
unit: true,
|
||||||
// specify the other units that are required for this test
|
// specify the other units that are required for this test
|
||||||
// needs: ['component:foo', 'helper:bar']
|
needs: [
|
||||||
|
'component:gh-ed-editor',
|
||||||
|
'component:gh-ed-preview',
|
||||||
|
'helper:gh-count-words',
|
||||||
|
'helper:route-action',
|
||||||
|
'service:notifications'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
it('renders', function () {
|
it('renders', function () {
|
||||||
|
Loading…
Reference in New Issue
Block a user