Skip to content

Commit ead352d

Browse files
committed
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
1 parent 22206f4 commit ead352d

File tree

11 files changed

+237
-203
lines changed

11 files changed

+237
-203
lines changed

core/client/app/components/gh-ed-editor.js

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
3232

3333
this.setFocus();
3434

35-
this.sendAction('setEditor', this);
35+
this.attrs.setEditor(this);
3636

3737
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
3838
},
@@ -43,22 +43,6 @@ export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
4343
}
4444
},
4545

46-
/**
47-
* Disable editing in the textarea (used while an upload is in progress)
48-
*/
49-
disable() {
50-
let textarea = this.get('element');
51-
textarea.setAttribute('readonly', 'readonly');
52-
},
53-
54-
/**
55-
* Reenable editing in the textarea
56-
*/
57-
enable() {
58-
let textarea = this.get('element');
59-
textarea.removeAttribute('readonly');
60-
},
61-
6246
actions: {
6347
toggleCopyHTMLModal(generatedHTML) {
6448
this.attrs.toggleCopyHTMLModal(generatedHTML);

core/client/app/components/gh-editor.js

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import Ember from 'ember';
2+
import ShortcutsMixin from 'ghost/mixins/shortcuts';
3+
import imageManager from 'ghost/utils/ed-image-manager';
4+
import editorShortcuts from 'ghost/utils/editor-shortcuts';
25

36
const {Component, computed, run} = Ember;
47
const {equal} = computed;
58

6-
export default Component.extend({
9+
export default Component.extend(ShortcutsMixin, {
710
tagName: 'section',
8-
classNames: ['gh-view'],
11+
classNames: ['view-container', 'view-editor'],
912

13+
activeTab: 'markdown',
14+
editor: null,
15+
editorDisabled: undefined,
16+
editorScrollInfo: null, // updated when gh-ed-editor component scrolls
17+
height: null, // updated when markdown is rendered
18+
shouldFocusEditor: false,
1019
showCopyHTMLModal: false,
1120
copyHTMLModalContent: null,
1221

13-
// updated when gh-ed-editor component scrolls
14-
editorScrollInfo: null,
15-
// updated when markdown is rendered
16-
height: null,
17-
activeTab: 'markdown',
22+
shortcuts: editorShortcuts,
1823

1924
markdownActive: equal('activeTab', 'markdown'),
2025
previewActive: equal('activeTab', 'preview'),
@@ -25,8 +30,7 @@ export default Component.extend({
2530
// stays in sync
2631
scrollPosition: computed('editorScrollInfo', 'height', function () {
2732
let scrollInfo = this.get('editorScrollInfo');
28-
let $previewContent = this.get('$previewContent');
29-
let $previewViewPort = this.get('$previewViewPort');
33+
let {$previewContent, $previewViewPort} = this;
3034

3135
if (!scrollInfo || !$previewContent || !$previewViewPort) {
3236
return 0;
@@ -41,28 +45,76 @@ export default Component.extend({
4145
return previewPosition;
4246
}),
4347

44-
scheduleAfterRender() {
45-
run.scheduleOnce('afterRender', this, this.afterRenderEvent);
46-
},
47-
4848
didInsertElement() {
4949
this._super(...arguments);
50-
this.scheduleAfterRender();
50+
this.registerShortcuts();
51+
run.scheduleOnce('afterRender', this, this._cacheElements);
5152
},
5253

53-
afterRenderEvent() {
54-
let $previewViewPort = this.$('.js-entry-preview-content');
54+
willDestroyElement() {
55+
if (this.attrs.onTeardown) {
56+
this.attrs.onTeardown();
57+
}
58+
this.removeShortcuts();
59+
},
5560

61+
_cacheElements() {
5662
// cache these elements for use in other methods
57-
this.set('$previewViewPort', $previewViewPort);
58-
this.set('$previewContent', this.$('.js-rendered-markdown'));
63+
this.$previewViewPort = this.$('.js-entry-preview-content');
64+
this.$previewContent = this.$('.js-rendered-markdown');
5965
},
6066

6167
actions: {
6268
selectTab(tab) {
6369
this.set('activeTab', tab);
6470
},
6571

72+
updateScrollInfo(scrollInfo) {
73+
this.set('editorScrollInfo', scrollInfo);
74+
},
75+
76+
updateHeight(height) {
77+
this.set('height', height);
78+
},
79+
80+
// set from a `sendAction` on the gh-ed-editor component,
81+
// so that we get a reference for handling uploads.
82+
setEditor(editor) {
83+
this.set('editor', editor);
84+
},
85+
86+
disableEditor() {
87+
this.set('editorDisabled', true);
88+
},
89+
90+
enableEditor() {
91+
this.set('editorDisabled', undefined);
92+
},
93+
94+
// The actual functionality is implemented in utils/ed-editor-shortcuts
95+
editorShortcut(options) {
96+
if (this.editor.$().is(':focus')) {
97+
this.editor.shortcut(options.type);
98+
}
99+
},
100+
101+
// Match the uploaded file to a line in the editor, and update that line with a path reference
102+
// ensuring that everything ends up in the correct place and format.
103+
handleImgUpload(e, resultSrc) {
104+
let editor = this.get('editor');
105+
let editorValue = editor.getValue();
106+
let replacement = imageManager.getSrcRange(editorValue, e.target);
107+
let cursorPosition;
108+
109+
if (replacement) {
110+
cursorPosition = replacement.start + resultSrc.length + 1;
111+
if (replacement.needsParens) {
112+
resultSrc = `(${resultSrc})`;
113+
}
114+
editor.replaceSelection(resultSrc, replacement.start, replacement.end, cursorPosition);
115+
}
116+
},
117+
66118
toggleCopyHTMLModal(generatedHTML) {
67119
this.set('copyHTMLModalContent', generatedHTML);
68120
this.toggleProperty('showCopyHTMLModal');

core/client/app/mixins/ed-editor-scroll.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default Mixin.create({
6161
*/
6262
scrollHandler() {
6363
this.set('scrollThrottle', run.throttle(this, () => {
64-
this.sendAction('updateScrollInfo', this.getScrollInfo());
64+
this.attrs.updateScrollInfo(this.getScrollInfo());
6565
}, 10));
6666
},
6767

core/client/app/mixins/editor-base-controller.js

Lines changed: 18 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import Ember from 'ember';
22
import PostModel from 'ghost/models/post';
33
import boundOneWay from 'ghost/utils/bound-one-way';
4-
import imageManager from 'ghost/utils/ed-image-manager';
54

65
const {Mixin, RSVP, computed, inject, observer, run} = Ember;
76
const {alias} = computed;
@@ -17,7 +16,6 @@ PostModel.eachAttribute(function (name) {
1716
export default Mixin.create({
1817
_autoSaveId: null,
1918
_timedSaveId: null,
20-
editor: null,
2119
submitting: false,
2220

2321
showLeaveEditorModal: false,
@@ -126,7 +124,7 @@ export default Mixin.create({
126124
let markdown = model.get('markdown');
127125
let title = model.get('title');
128126
let titleScratch = model.get('titleScratch');
129-
let scratch = this.get('editor').getValue();
127+
let scratch = this.get('model.scratch');
130128
let changedAttributes;
131129

132130
if (!this.tagNamesEqual()) {
@@ -253,23 +251,29 @@ export default Mixin.create({
253251
},
254252

255253
actions: {
254+
cancelTimers() {
255+
let autoSaveId = this._autoSaveId;
256+
let timedSaveId = this._timedSaveId;
257+
258+
if (autoSaveId) {
259+
run.cancel(autoSaveId);
260+
this._autoSaveId = null;
261+
}
262+
263+
if (timedSaveId) {
264+
run.cancel(timedSaveId);
265+
this._timedSaveId = null;
266+
}
267+
},
268+
256269
save(options) {
257270
let prevStatus = this.get('model.status');
258271
let isNew = this.get('model.isNew');
259-
let autoSaveId = this._autoSaveId;
260-
let timedSaveId = this._timedSaveId;
261272
let psmController = this.get('postSettingsMenuController');
262273
let promise, status;
263274

264275
options = options || {};
265276

266-
// when navigating quickly between pages autoSave will occasionally
267-
// try to run after the editor has been torn down so bail out here
268-
// before we throw errors
269-
if (!this.get('editor').$()) {
270-
return 0;
271-
}
272-
273277
this.toggleProperty('submitting');
274278

275279
if (options.backgroundSave) {
@@ -279,19 +283,11 @@ export default Mixin.create({
279283
status = this.get('willPublish') ? 'published' : 'draft';
280284
}
281285

282-
if (autoSaveId) {
283-
run.cancel(autoSaveId);
284-
this._autoSaveId = null;
285-
}
286-
287-
if (timedSaveId) {
288-
run.cancel(timedSaveId);
289-
this._timedSaveId = null;
290-
}
286+
this.send('cancelTimers');
291287

292288
// Set the properties that are indirected
293289
// set markdown equal to what's in the editor, minus the image markers.
294-
this.set('model.markdown', this.get('editor').getValue());
290+
this.set('model.markdown', this.get('model.scratch'));
295291
this.set('model.status', status);
296292

297293
// Set a default title
@@ -345,53 +341,12 @@ export default Mixin.create({
345341
}
346342
},
347343

348-
// set from a `sendAction` on the gh-ed-editor component,
349-
// so that we get a reference for handling uploads.
350-
setEditor(editor) {
351-
this.set('editor', editor);
352-
},
353-
354-
// fired from the gh-ed-preview component when an image upload starts
355-
disableEditor() {
356-
this.get('editor').disable();
357-
},
358-
359-
// fired from the gh-ed-preview component when an image upload finishes
360-
enableEditor() {
361-
this.get('editor').enable();
362-
},
363-
364-
// Match the uploaded file to a line in the editor, and update that line with a path reference
365-
// ensuring that everything ends up in the correct place and format.
366-
handleImgUpload(e, resultSrc) {
367-
let editor = this.get('editor');
368-
let editorValue = editor.getValue();
369-
let replacement = imageManager.getSrcRange(editorValue, e.target);
370-
let cursorPosition;
371-
372-
if (replacement) {
373-
cursorPosition = replacement.start + resultSrc.length + 1;
374-
if (replacement.needsParens) {
375-
resultSrc = `(${resultSrc})`;
376-
}
377-
editor.replaceSelection(resultSrc, replacement.start, replacement.end, cursorPosition);
378-
}
379-
},
380-
381344
autoSaveNew() {
382345
if (this.get('model.isNew')) {
383346
this.send('save', {silent: true, backgroundSave: true});
384347
}
385348
},
386349

387-
updateEditorScrollInfo(scrollInfo) {
388-
this.set('editorScrollInfo', scrollInfo);
389-
},
390-
391-
updateHeight(height) {
392-
this.set('height', height);
393-
},
394-
395350
toggleLeaveEditorModal(transition) {
396351
this.set('leaveEditorTransition', transition);
397352
this.toggleProperty('showLeaveEditorModal');

core/client/app/mixins/editor-base-route.js

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import Ember from 'ember';
22
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
33
import styleBody from 'ghost/mixins/style-body';
4-
import editorShortcuts from 'ghost/utils/editor-shortcuts';
4+
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
55

66
const {Mixin, RSVP, run} = Ember;
77

8+
let generalShortcuts = {};
9+
generalShortcuts[`${ctrlOrCmd}+alt+p`] = 'publish';
10+
generalShortcuts['alt+shift+z'] = 'toggleZenMode';
11+
812
export default Mixin.create(styleBody, ShortcutsRoute, {
913
classNames: ['editor'],
1014

15+
shortcuts: generalShortcuts,
16+
1117
actions: {
1218
save() {
1319
this.get('controller').send('save');
@@ -24,14 +30,6 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
2430
Ember.$('body').toggleClass('zen');
2531
},
2632

27-
// The actual functionality is implemented in utils/ed-editor-shortcuts
28-
editorShortcut(options) {
29-
// Only fire editor shortcuts when the editor has focus.
30-
if (this.get('controller.editor').$().is(':focus')) {
31-
this.get('controller.editor').shortcut(options.type);
32-
}
33-
},
34-
3533
willTransition(transition) {
3634
let controller = this.get('controller');
3735
let scratch = controller.get('model.scratch');
@@ -97,8 +95,6 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
9795
});
9896
},
9997

100-
shortcuts: editorShortcuts,
101-
10298
attachModelHooks(controller, model) {
10399
// this will allow us to track when the model is saved and update the controller
104100
// so that we can be sure controller.hasDirtyAttributes is correct, without having to update the

0 commit comments

Comments
 (0)