Reach Editor parity with Ember

closes #2426, closes #2781, closes #2913
- Concatenate vendor files on change of js in core/shared/
- Add all the markerManager stuff to its own mixin
- make markers a shared object for all that mix it in. makes it easier to use helper functions in different modules
- add getMarkdown method, returns object with two keys holding the markdown: one with markers, the other without
- Clear markers when codemirror is destroyed
- make Editor subcomponents communicate through the Editor Controller
- Set Codemirror and html preview shared scrolling
- Set CodeMirror, html preview css scroll class with util
- Create 'scratch' property in Editor controller; prevents a model save wiping image markers due to markdown bindings
- Add editor and html preview actions to handle img upload start/finish
- disable codemirror when an image is being uploaded, enables on success or failure
- Fix editor wordcount when there are 0 words
- Add modal dialog when transitioning out of the editor with an unsaved post
- Add window.onbeforeunload handling with `.unloadDirtyMessage()` on editor controller
- and various other things
This commit is contained in:
David Arvelo 2014-06-05 21:18:03 -04:00
parent f73f6125eb
commit eb949aafae
18 changed files with 738 additions and 43 deletions

View File

@ -62,6 +62,10 @@ var path = require('path'),
files: ['core/clientold/tpl/**/*.hbs'],
tasks: ['handlebars']
},
shared: {
files: ['core/shared/**/*.js'],
tasks: ['concat:dev', 'concat:dev-ember']
},
'handlebars-ember': {
files: ['core/client/**/*.hbs'],
tasks: ['emberTemplates:dev']

View File

@ -1,34 +1,99 @@
/* global CodeMirror*/
import MarkerManager from 'ghost/mixins/marker-manager';
import setScrollClassName from 'ghost/utils/set-scroll-classname';
var onChangeHandler = function (cm, changeObj) {
var line,
component = cm.component,
checkLine = component.checkLine.bind(component),
checkMarkers = component.checkMarkers.bind(component);
// fill array with a range of numbers
for (line = changeObj.from.line; line < changeObj.from.line + changeObj.text.length; line += 1) {
checkLine(line, changeObj.origin);
}
// Is this a line which may have had a marker on it?
checkMarkers();
var onChangeHandler = function (cm) {
cm.component.set('value', cm.getDoc().getValue());
};
var onScrollHandler = function (cm) {
var scrollInfo = cm.getScrollInfo(),
percentage = scrollInfo.top / scrollInfo.height,
component = cm.component;
scrollInfo.codemirror = cm;
// throttle scroll updates
component.throttle = Ember.run.throttle(component, function () {
this.set('scrollPosition', percentage);
}, 50);
this.set('scrollInfo', scrollInfo);
}, 10);
};
var Codemirror = Ember.TextArea.extend({
var Codemirror = Ember.TextArea.extend(MarkerManager, {
didInsertElement: function () {
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
},
afterRenderEvent: function () {
var initMarkers = this.initMarkers.bind(this);
this.initCodemirror();
this.codemirror.eachLine(initMarkers);
this.sendAction('action', this);
},
// this needs to be placed on the 'afterRender' queue otherwise CodeMirror gets wonky
initCodemirror: function () {
// create codemirror
this.codemirror = CodeMirror.fromTextArea(this.get('element'), {
lineWrapping: true
var codemirror = CodeMirror.fromTextArea(this.get('element'), {
mode: 'gfm',
tabMode: 'indent',
tabindex: '2',
cursorScrollMargin: 10,
lineWrapping: true,
dragDrop: false,
extraKeys: {
Home: 'goLineLeft',
End: 'goLineRight'
}
});
this.codemirror.component = this; // save reference to this
codemirror.component = this; // save reference to this
// propagate changes to value property
this.codemirror.on('change', onChangeHandler);
codemirror.on('change', onChangeHandler);
// on scroll update scrollPosition property
this.codemirror.on('scroll', onScrollHandler);
}.on('didInsertElement'),
codemirror.on('scroll', onScrollHandler);
codemirror.on('scroll', Ember.run.bind(Ember.$('.CodeMirror-scroll'), setScrollClassName, {
target: Ember.$('.entry-markdown'),
offset: 10
}));
this.set('codemirror', codemirror);
},
disableCodeMirror: function () {
var codemirror = this.get('codemirror');
codemirror.setOption('readOnly', 'nocursor');
codemirror.off('change', onChangeHandler);
},
enableCodeMirror: function () {
var codemirror = this.get('codemirror');
codemirror.setOption('readOnly', false);
// clicking the trash button on an image dropzone causes this function to fire.
// this line is a hack to prevent multiple event handlers from being attached.
codemirror.off('change', onChangeHandler);
codemirror.on('change', onChangeHandler);
},
removeThrottle: function () {
Ember.run.cancel(this.throttle);
@ -36,8 +101,13 @@ var Codemirror = Ember.TextArea.extend({
removeCodemirrorHandlers: function () {
// not sure if this is needed.
this.codemirror.off('change', onChangeHandler);
this.codemirror.off('scroll', onScrollHandler);
var codemirror = this.get('codemirror');
codemirror.off('change', onChangeHandler);
codemirror.off('scroll');
}.on('willDestroyElement'),
clearMarkerManagerMarkers: function () {
this.clearMarkers();
}.on('willDestroyElement')
});

View File

@ -1,11 +1,36 @@
var Markdown = Ember.Component.extend({
adjustScrollPosition: function () {
var scrollWrapper = this.$().closest('.entry-preview-content').get(0),
// calculate absolute scroll position from percentage
scrollPixel = scrollWrapper.scrollHeight * this.get('scrollPosition');
import uploader from 'ghost/assets/lib/uploader';
scrollWrapper.scrollTop = scrollPixel; // adjust scroll position
}.observes('scrollPosition')
var Markdown = Ember.Component.extend({
classNames: ['rendered-markdown'],
didInsertElement: function () {
this.set('scrollWrapper', this.$().closest('.entry-preview-content'));
},
adjustScrollPosition: function () {
var scrollWrapper = this.get('scrollWrapper'),
scrollPosition = this.get('scrollPosition');
scrollWrapper.scrollTop(scrollPosition);
}.observes('scrollPosition'),
// fire off 'enable' API function from uploadManager
// might need to make sure markdown has been processed first
reInitDropzones: function () {
Ember.run.scheduleOnce('afterRender', this, function () {
var dropzones = $('.js-drop-zone');
uploader.call(dropzones, {
editor: true,
filestorage: false
});
dropzones.on('uploadstart', this.sendAction.bind(this, 'uploadStarted'));
dropzones.on('uploadfailure', this.sendAction.bind(this, 'uploadFinished'));
dropzones.on('uploadsuccess', this.sendAction.bind(this, 'uploadFinished'));
dropzones.on('uploadsuccess', this.sendAction.bind(this, 'uploadSuccess'));
});
}.observes('markdown')
});
export default Markdown;
export default Markdown;

View File

@ -1,5 +1,16 @@
import EditorControllerMixin from 'ghost/mixins/editor-base-controller';
import MarkerManager from 'ghost/mixins/marker-manager';
var EditorEditController = Ember.ObjectController.extend(EditorControllerMixin);
var EditorEditController = Ember.ObjectController.extend(EditorControllerMixin, MarkerManager, {
init: function () {
var self = this;
this._super();
window.onbeforeunload = function () {
return self.get('isDirty') ? self.unloadDirtyMessage() : null;
};
}
});
export default EditorEditController;

View File

@ -1,6 +1,17 @@
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
@ -17,4 +28,4 @@ var EditorNewController = Ember.ObjectController.extend(EditorControllerMixin, {
}
});
export default EditorNewController;
export default EditorNewController;

View File

@ -0,0 +1,59 @@
var LeaveEditorController = Ember.Controller.extend({
args: Ember.computed.alias('model'),
actions: {
confirmAccept: function () {
var args = this.get('args'),
editorController,
model,
transition;
if (Ember.isArray(args)) {
editorController = args[0];
transition = args[1];
model = editorController.get('model');
}
// @TODO: throw some kind of error here? return true will send it upward?
if (!transition || !editorController) {
return true;
}
// definitely want to clear the data store and post of any unsaved, client-generated tags
editorController.updateTags();
if (model.get('isNew')) {
// the user doesn't want to save the new, unsaved post, so delete it.
model.deleteRecord();
} else {
// roll back changes on model props
model.rollback();
}
// setting isDirty to false here allows willTransition on the editor route to succeed
editorController.set('isDirty', false);
// since the transition is now certain to complete, we can unset window.onbeforeunload here
window.onbeforeunload = null;
transition.retry();
},
confirmReject: function () {
}
},
confirm: {
accept: {
text: 'Leave',
buttonClass: 'button-delete'
},
reject: {
text: 'Cancel',
buttonClass: 'button'
}
}
});
export default LeaveEditorController;

View File

@ -1,7 +1,12 @@
import count from 'ghost/utils/word-count';
import counter from 'ghost/utils/word-count';
var countWords = Ember.Handlebars.makeBoundHelper(function (markdown) {
return count(markdown || '');
if (/^\s*$/.test(markdown)) {
return '0 words';
}
var count = counter(markdown || '');
return count + (count === 1 ? ' word' : ' words');
});
export default countWords;

View File

@ -1,5 +1,5 @@
/* global Showdown, Handlebars */
var showdown = new Showdown.converter();
var showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm']});
var formatMarkdown = Ember.Handlebars.makeBoundHelper(function (markdown) {
return new Handlebars.SafeString(showdown.makeHtml(markdown || ''));

View File

@ -1,6 +1,19 @@
/* global console */
import MarkerManager from 'ghost/mixins/marker-manager';
import PostModel from 'ghost/models/post';
var EditorControllerMixin = Ember.Mixin.create({
// this array will hold properties we need to watch
// to know if the model has been changed (`controller.isDirty`)
var watchedProps = ['scratch', 'model.isDirty'];
Ember.get(PostModel, 'attributes').forEach(function (name) {
watchedProps.push('model.' + name);
});
// watch if number of tags changes on the model
watchedProps.push('tags.[]');
var EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
/**
* By default, a post will not change its publish state.
* Only with a user-set value (via setSaveType action)
@ -13,6 +26,90 @@ var EditorControllerMixin = Ember.Mixin.create({
return this.get('isPublished');
}.property('isPublished'),
// set by the editor route and `isDirty`. useful when checking
// whether the number of tags has changed for `isDirty`.
previousTagNames: null,
tagNames: function () {
return this.get('tags').mapBy('name');
}.property('tags.[]'),
// compares previousTagNames to tagNames
tagNamesEqual: function () {
var tagNames = this.get('tagNames'),
previousTagNames = this.get('previousTagNames'),
hashCurrent,
hashPrevious;
// beware! even if they have the same length,
// that doesn't mean they're the same.
if (tagNames.length !== previousTagNames.length) {
return false;
}
// instead of comparing with slow, nested for loops,
// perform join on each array and compare the strings
hashCurrent = tagNames.join('');
hashPrevious = previousTagNames.join('');
return hashCurrent === hashPrevious;
},
// an ugly hack, but necessary to watch all the model's properties
// and more, without having to be explicit and do it manually
isDirty: Ember.computed.apply(Ember, watchedProps.concat(function (key, value) {
if (arguments.length > 1) {
return value;
}
var model = this.get('model'),
markdown = this.get('markdown'),
scratch = this.getMarkdown().withoutMarkers,
changedAttributes;
if (!this.tagNamesEqual()) {
this.set('previousTagNames', this.get('tagNames'));
return true;
}
// since `scratch` is not model property, we need to check
// it explicitly against the model's markdown attribute
if (markdown !== scratch) {
return true;
}
// models created on the client always return `isDirty: true`,
// so we need to see which properties have actually changed.
if (model.get('isNew')) {
changedAttributes = Ember.keys(model.changedAttributes());
if (changedAttributes.length) {
return true;
}
return false;
}
// even though we use the `scratch` prop to show edits,
// which does *not* change the model's `isDirty` property,
// `isDirty` will tell us if the other props have changed,
// as long as the model is not new (model.isNew === false).
if (model.get('isDirty')) {
return true;
}
return false;
})),
// used on window.onbeforeunload
unloadDirtyMessage: function () {
return '==============================\n\n' +
'Hey there! It looks like you\'re in the middle of writing' +
' something and you haven\'t saved all of your content.' +
'\n\nSave before you go!\n\n' +
'==============================';
},
// remove client-generated tags, which have `id: null`.
// Ember Data won't recognize/update them automatically
// when returned from the server with ids.
@ -29,9 +126,15 @@ var EditorControllerMixin = Ember.Mixin.create({
var status = this.get('willPublish') ? 'published' : 'draft',
self = this;
// set markdown equal to what's in the editor, minus the image markers.
this.set('markdown', this.getMarkdown().withoutMarkers);
this.set('status', status);
return this.get('model').save().then(function (model) {
self.updateTags();
// `updateTags` triggers `isDirty => true`.
// for a saved model it would otherwise be false.
self.set('isDirty', false);
self.notifications.showSuccess('Post status saved as <strong>' +
model.get('status') + '</strong>.');
@ -47,6 +150,57 @@ var EditorControllerMixin = Ember.Mixin.create({
} else {
console.warn('Received invalid save type; ignoring.');
}
},
// set from a `sendAction` on the codemirror component,
// so that we get a reference for handling uploads.
setCodeMirror: function (codemirrorComponent) {
var codemirror = codemirrorComponent.get('codemirror');
this.set('codemirrorComponent', codemirrorComponent);
this.set('codemirror', codemirror);
},
// fired from the gh-markdown component when an image upload starts
disableCodeMirror: function () {
this.get('codemirrorComponent').disableCodeMirror();
},
// fired from the gh-markdown component when an image upload finishes
enableCodeMirror: function () {
this.get('codemirrorComponent').enableCodeMirror();
},
// 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: function (e, result_src) {
var editor = this.get('codemirror'),
line = this.findLine(Ember.$(e.currentTarget).attr('id')),
lineNumber = editor.getLineNumber(line),
match = line.text.match(/\([^\n]*\)?/),
replacement = '(http://)';
if (match) {
// simple case, we have the parenthesis
editor.setSelection(
{line: lineNumber, ch: match.index + 1},
{line: lineNumber, ch: match.index + match[0].length - 1}
);
} else {
match = line.text.match(/\]/);
if (match) {
editor.replaceRange(
replacement,
{line: lineNumber, ch: match.index + 1},
{line: lineNumber, ch: match.index + 1}
);
editor.setSelection(
{line: lineNumber, ch: match.index + 2},
{line: lineNumber, ch: match.index + replacement.length }
);
}
}
editor.replaceSelection(result_src);
}
}
});

View File

@ -0,0 +1,58 @@
import setScrollClassName from 'ghost/utils/set-scroll-classname';
var EditorViewMixin = Ember.Mixin.create({
// create a hook for jQuery logic that will run after
// a view and all child views have been rendered,
// since didInsertElement runs only when the view's el
// has rendered, and not necessarily all child views.
//
// http://mavilein.github.io/javascript/2013/08/01/Ember-JS-After-Render-Event/
// http://emberjs.com/api/classes/Ember.run.html#method_next
scheduleAfterRender: function () {
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
}.on('didInsertElement'),
// all child views will have rendered when this fires
afterRenderEvent: function () {
var $previewViewPort = this.$('.entry-preview-content');
// cache these elements for use in other methods
this.set('$previewViewPort', $previewViewPort);
this.set('$previewContent', this.$('.rendered-markdown'));
$previewViewPort.scroll(Ember.run.bind($previewViewPort, setScrollClassName, {
target: this.$('.entry-preview'),
offset: 10
}));
},
removeScrollHandlers: function () {
this.get('$previewViewPort').off('scroll');
}.on('willDestroyElement'),
// updated when gh-codemirror component scrolls
markdownScrollInfo: null,
// percentage of scroll position to set htmlPreview
scrollPosition: Ember.computed('markdownScrollInfo', function () {
if (!this.get('markdownScrollInfo')) {
return 0;
}
var scrollInfo = this.get('markdownScrollInfo'),
codemirror = scrollInfo.codemirror,
markdownHeight = scrollInfo.height - scrollInfo.clientHeight,
previewHeight = this.get('$previewContent').height() - this.get('$previewViewPort').height(),
ratio = previewHeight / markdownHeight,
previewPosition = scrollInfo.top * ratio,
isCursorAtEnd = codemirror.getCursor('end').line > codemirror.lineCount() - 5;
if (isCursorAtEnd) {
previewPosition = previewHeight + 30;
}
return previewPosition;
})
});
export default EditorViewMixin;

View File

@ -0,0 +1,219 @@
var MarkerManager = Ember.Mixin.create({
imageMarkdownRegex: /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
markerRegex: /\{<([\w\W]*?)>\}/,
uploadId: 1,
// create an object that will be shared amongst instances.
// makes it easier to use helper functions in different modules
markers: {},
// Add markers to the line if it needs one
initMarkers: function (line) {
var imageMarkdownRegex = this.get('imageMarkdownRegex'),
markerRegex = this.get('markerRegex'),
editor = this.get('codemirror'),
isImage = line.text.match(imageMarkdownRegex),
hasMarker = line.text.match(markerRegex);
if (isImage && !hasMarker) {
this.addMarker(line, editor.getLineNumber(line));
}
},
// Get the markdown with all the markers stripped
getMarkdown: function (value) {
var marker, id,
editor = this.get('codemirror'),
markers = this.get('markers'),
markerRegexForId = this.get('markerRegexForId'),
oldValue = value || editor.getValue(),
newValue = oldValue;
for (id in markers) {
if (markers.hasOwnProperty(id)) {
marker = markers[id];
newValue = newValue.replace(markerRegexForId(id), '');
}
}
return {
withMarkers: oldValue,
withoutMarkers: newValue
};
},
// check the given line to see if it has an image, and if it correctly has a marker
// in the special case of lines which were just pasted in, any markers are removed to prevent duplication
checkLine: function (ln, mode) {
var editor = this.get('codemirror'),
line = editor.getLineHandle(ln),
imageMarkdownRegex = this.get('imageMarkdownRegex'),
markerRegex = this.get('markerRegex'),
isImage = line.text.match(imageMarkdownRegex),
hasMarker;
// We care if it is an image
if (isImage) {
hasMarker = line.text.match(markerRegex);
if (hasMarker && (mode === 'paste' || mode === 'undo')) {
// this could be a duplicate, and won't be a real marker
this.stripMarkerFromLine(line);
}
if (!hasMarker) {
this.addMarker(line, ln);
}
}
// TODO: hasMarker but no image?
},
// Add a marker to the given line
// Params:
// line - CodeMirror LineHandle
// ln - line number
addMarker: function (line, ln) {
var marker,
markers = this.get('markers'),
editor = this.get('codemirror'),
uploadPrefix = 'image_upload',
uploadId = this.get('uploadId'),
magicId = '{<' + uploadId + '>}',
newText = magicId + line.text;
editor.replaceRange(
newText,
{line: ln, ch: 0},
{line: ln, ch: newText.length}
);
marker = editor.markText(
{line: ln, ch: 0},
{line: ln, ch: (magicId.length)},
{collapsed: true}
);
markers[uploadPrefix + '_' + uploadId] = marker;
this.set('uploadId', uploadId += 1);
},
// Check each marker to see if it is still present in the editor and if it still corresponds to image markdown
// If it is no longer a valid image, remove it
checkMarkers: function () {
var id, marker, line,
editor = this.get('codemirror'),
markers = this.get('markers'),
imageMarkdownRegex = this.get('imageMarkdownRegex');
for (id in markers) {
if (markers.hasOwnProperty(id)) {
marker = markers[id];
if (marker.find()) {
line = editor.getLineHandle(marker.find().from.line);
if (!line.text.match(imageMarkdownRegex)) {
this.removeMarker(id, marker, line);
}
} else {
this.removeMarker(id, marker);
}
}
}
},
// this is needed for when we transition out of the editor.
// since the markers object is persistent and shared between classes that
// mix in this mixin, we need to make sure markers don't carry over between edits.
clearMarkers: function () {
var markers = this.get('markers'),
id,
marker;
// can't just `this.set('markers', {})`,
// since it wouldn't apply to this mixin,
// but only to the class that mixed this mixin in
for (id in markers) {
if (markers.hasOwnProperty(id)) {
marker = markers[id];
delete markers[id];
marker.clear();
}
}
},
// Remove a marker
// Will be passed a LineHandle if we already know which line the marker is on
removeMarker: function (id, marker, line) {
var markers = this.get('markers');
delete markers[id];
marker.clear();
if (line) {
this.stripMarkerFromLine(line);
} else {
this.findAndStripMarker(id);
}
},
// Removes the marker on the given line if there is one
stripMarkerFromLine: function (line) {
var ln,
editor = this.get('codemirror'),
markerRegex = /\{<([\w\W]*?)>\}/,
markerText = line.text.match(markerRegex);
ln = editor.getLineNumber(line);
if (markerText) {
editor.replaceRange(
'',
{line: ln, ch: markerText.index},
{line: ln, ch: markerText.index + markerText[0].length}
);
}
},
// the regex
markerRegexForId: function (id) {
id = id.replace('image_upload_', '');
return new RegExp('\\{<' + id + '>\\}', 'gmi');
},
// Find a marker in the editor by id & remove it
// Goes line by line to find the marker by it's text if we've lost track of the TextMarker
findAndStripMarker: function (id) {
var self = this,
editor = this.get('codemirror');
editor.eachLine(function (line) {
var markerText = self.markerRegexForId(id).exec(line.text),
ln;
if (markerText) {
ln = editor.getLineNumber(line);
editor.replaceRange(
'',
{line: ln, ch: markerText.index},
{line: ln, ch: markerText.index + markerText[0].length}
);
}
});
},
// Find the line with the marker which matches
findLine: function (result_id) {
var editor = this.get('codemirror'),
markers = this.get('markers');
// try to find the right line to replace
if (markers.hasOwnProperty(result_id) && markers[result_id].find()) {
return editor.getLineHandle(markers[result_id].find().from.line);
}
return false;
}
});
export default MarkerManager;

View File

@ -8,6 +8,7 @@ var EditorEditRoute = AuthenticatedRoute.extend(styleBody, {
var self = this,
post,
postId;
postId = Number(params.post_id);
if (!Number.isInteger(postId) || !Number.isFinite(postId) || postId <= 0) {
@ -33,8 +34,42 @@ var EditorEditRoute = AuthenticatedRoute.extend(styleBody, {
return self.transitionTo('posts.index');
});
},
serialize: function (model) {
return {post_id: model.get('id')};
},
setupController: function (controller, model) {
this._super(controller, model);
controller.set('scratch', model.get('markdown'));
model.get('tags').then(function (tags) {
// used to check if anything has changed in the editor
controller.set('previousTagNames', tags.mapBy('name'));
});
},
actions: {
willTransition: function (transition) {
var controller = this.get('controller'),
isDirty = controller.get('isDirty'),
model = controller.get('model'),
isSaving = model.get('isSaving'),
isDeleted = model.get('isDeleted');
// when `isDeleted && isSaving`, model is in-flight, being saved
// to the server. in that case we can probably just transition
// now and have the server return the record, thereby updating it
if (!(isDeleted && isSaving) && isDirty) {
transition.abort();
this.send('openModal', 'leave-editor', [controller, transition]);
return;
}
// since the transition is now certain to complete..
window.onbeforeunload = null;
}
}
});

View File

@ -5,9 +5,43 @@ var EditorNewRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['editor'],
model: function () {
return this.store.createRecord('post', {
title: ''
});
return this.store.createRecord('post');
},
setupController: function (controller, model) {
this._super(controller, model);
controller.set('scratch', '');
// used to check if anything has changed in the editor
controller.set('previousTagNames', Ember.A());
},
actions: {
willTransition: function (transition) {
var controller = this.get('controller'),
isDirty = controller.get('isDirty'),
model = controller.get('model'),
isNew = model.get('isNew'),
isSaving = model.get('isSaving'),
isDeleted = model.get('isDeleted');
// when `isDeleted && isSaving`, model is in-flight, being saved
// to the server. in that case we can probably just transition
// now and have the server return the record, thereby updating it
if (!(isDeleted && isSaving) && isDirty) {
transition.abort();
this.send('openModal', 'leave-editor', [controller, transition]);
return;
}
if (isNew) {
model.deleteRecord();
}
// since the transition is now certain to complete..
window.onbeforeunload = null;
}
}
});

View File

@ -1,3 +1 @@
<div class="rendered-markdown">
{{gh-format-markdown markdown}}
</div>
{{gh-format-markdown markdown}}

View File

@ -10,16 +10,17 @@
<a class="markdown-help" href="" {{action "openModal" "markdown"}}><span class="hidden">What is Markdown?</span></a>
</header>
<section id="entry-markdown-content" class="entry-markdown-content">
{{gh-codemirror value=markdown scrollPosition=view.scrollPosition}}
{{gh-codemirror value=scratch scrollInfo=view.markdownScrollInfo action="setCodeMirror"}}
</section>
</section>
<section class="entry-preview">
<header class="floatingheader">
<small>Preview <span class="entry-word-count js-entry-word-count">{{gh-count-words markdown}} words</span></small>
<small>Preview <span class="entry-word-count js-entry-word-count">{{gh-count-words scratch}}</span></small>
</header>
<section class="entry-preview-content">
{{gh-markdown markdown=markdown scrollPosition=view.scrollPosition}}
{{gh-markdown markdown=scratch scrollPosition=view.scrollPosition
uploadStarted="disableCodeMirror" uploadFinished="enableCodeMirror" uploadSuccess="handleImgUpload"}}
</section>
</section>

View File

@ -0,0 +1,9 @@
{{#gh-modal-dialog action="closeModal" showClose=true type="action" style="wide,centered" animation="fade"
title="Are you sure you want to leave this page?" confirm=confirm}}
<p>Hey there! It looks like you're in the middle of writing something and you haven't saved all of your
content.</p>
<p>Save before you go!</p>
{{/gh-modal-dialog}}

View File

@ -1,7 +1,8 @@
var EditorView = Ember.View.extend({
import EditorViewMixin from 'ghost/mixins/editor-base-view';
var EditorView = Ember.View.extend(EditorViewMixin, {
tagName: 'section',
classNames: ['entry-container'],
scrollPosition: 0 // percentage of scroll position
classNames: ['entry-container']
});
export default EditorView;

View File

@ -1,8 +1,9 @@
var EditorNewView = Ember.View.extend({
import EditorViewMixin from 'ghost/mixins/editor-base-view';
var EditorNewView = Ember.View.extend(EditorViewMixin, {
tagName: 'section',
templateName: 'editor/edit',
classNames: ['entry-container'],
scrollPosition: 0 // percentage of scroll position
classNames: ['entry-container']
});
export default EditorNewView;