mirror of
https://github.com/TryGhost/Ghost-Admin.git
synced 2023-12-14 02:33:04 +01:00
No more CodeMirror
closes #4368, fixes #1240 (spellcheck), fixes #4974 & fixes #4983 (caret positioning bugs) - Drop CodeMirror in favour of a plain text area - Use rangyinputs to handle selections cross-browser - Create an API for interacting with the textarea - Replace marker manager with a much simpler image manager - Reimplement shortcuts, including some bug fixes
This commit is contained in:
parent
3c927411fa
commit
48996c767b
24 changed files with 687 additions and 1049 deletions
|
@ -34,10 +34,7 @@ app.import('bower_components/jquery/dist/jquery.js');
|
|||
app.import('bower_components/ic-ajax/dist/globals/main.js');
|
||||
app.import('bower_components/ember-load-initializers/ember-load-initializers.js');
|
||||
app.import('bower_components/validator-js/validator.js');
|
||||
app.import('bower_components/codemirror/lib/codemirror.js');
|
||||
app.import('bower_components/codemirror/addon/mode/overlay.js');
|
||||
app.import('bower_components/codemirror/mode/markdown/markdown.js');
|
||||
app.import('bower_components/codemirror/mode/gfm/gfm.js');
|
||||
app.import('bower_components/rangyinputs/rangyinputs-jquery-src.js');
|
||||
app.import('bower_components/showdown-ghost/src/showdown.js');
|
||||
app.import('bower_components/moment/moment.js');
|
||||
app.import('bower_components/keymaster/keymaster.js');
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
var createTouchEditor = function createTouchEditor() {
|
||||
var noop = function () {},
|
||||
TouchEditor;
|
||||
|
||||
TouchEditor = function (el, options) {
|
||||
/*jshint unused:false*/
|
||||
this.textarea = el;
|
||||
this.win = {document: this.textarea};
|
||||
this.ready = true;
|
||||
this.wrapping = document.createElement('div');
|
||||
|
||||
var textareaParent = this.textarea.parentNode;
|
||||
|
||||
this.wrapping.appendChild(this.textarea);
|
||||
textareaParent.appendChild(this.wrapping);
|
||||
|
||||
this.textarea.style.opacity = 1;
|
||||
};
|
||||
|
||||
TouchEditor.prototype = {
|
||||
setOption: function (type, handler) {
|
||||
if (type === 'onChange') {
|
||||
$(this.textarea).change(handler);
|
||||
}
|
||||
},
|
||||
eachLine: function () {
|
||||
return [];
|
||||
},
|
||||
getValue: function () {
|
||||
return this.textarea.value;
|
||||
},
|
||||
setValue: function (code) {
|
||||
this.textarea.value = code;
|
||||
},
|
||||
focus: noop,
|
||||
getCursor: function () {
|
||||
return {line: 0, ch: 0};
|
||||
},
|
||||
setCursor: noop,
|
||||
currentLine: function () {
|
||||
return 0;
|
||||
},
|
||||
cursorPosition: function () {
|
||||
return {character: 0};
|
||||
},
|
||||
addMarkdown: noop,
|
||||
nthLine: noop,
|
||||
refresh: noop,
|
||||
selectLines: noop,
|
||||
on: noop,
|
||||
off: noop
|
||||
};
|
||||
|
||||
return TouchEditor;
|
||||
};
|
||||
|
||||
export default createTouchEditor;
|
|
@ -1,164 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
/*global CodeMirror */
|
||||
|
||||
import MarkerManager from 'ghost/mixins/marker-manager';
|
||||
import mobileCodeMirror from 'ghost/utils/codemirror-mobile';
|
||||
import setScrollClassName from 'ghost/utils/set-scroll-classname';
|
||||
import codeMirrorShortcuts from 'ghost/utils/codemirror-shortcuts';
|
||||
|
||||
var onChangeHandler,
|
||||
onScrollHandler,
|
||||
Codemirror;
|
||||
|
||||
codeMirrorShortcuts.init();
|
||||
|
||||
onChangeHandler = function (cm, changeObj) {
|
||||
var line,
|
||||
component = cm.component;
|
||||
|
||||
// fill array with a range of numbers
|
||||
for (line = changeObj.from.line; line < changeObj.from.line + changeObj.text.length; line += 1) {
|
||||
component.checkLine.call(component, line, changeObj.origin);
|
||||
}
|
||||
|
||||
// Is this a line which may have had a marker on it?
|
||||
component.checkMarkers.call(component);
|
||||
|
||||
cm.component.set('value', cm.getValue());
|
||||
|
||||
component.sendAction('typingPause');
|
||||
};
|
||||
|
||||
onScrollHandler = function (cm) {
|
||||
var scrollInfo = cm.getScrollInfo(),
|
||||
component = cm.component;
|
||||
|
||||
scrollInfo.codemirror = cm;
|
||||
|
||||
// throttle scroll updates
|
||||
component.throttle = Ember.run.throttle(component, function () {
|
||||
this.set('scrollInfo', scrollInfo);
|
||||
}, 10);
|
||||
};
|
||||
|
||||
Codemirror = Ember.TextArea.extend(MarkerManager, {
|
||||
focus: true,
|
||||
focusCursorAtEnd: false,
|
||||
|
||||
setFocus: function () {
|
||||
if (this.get('focus')) {
|
||||
this.$().val(this.$().val()).focus();
|
||||
}
|
||||
}.on('didInsertElement'),
|
||||
|
||||
didInsertElement: function () {
|
||||
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
|
||||
},
|
||||
|
||||
afterRenderEvent: function () {
|
||||
var self = this,
|
||||
codemirror;
|
||||
|
||||
// replaces CodeMirror with TouchEditor only if we're on mobile
|
||||
mobileCodeMirror.createIfMobile();
|
||||
|
||||
codemirror = this.initCodemirror();
|
||||
this.set('codemirror', codemirror);
|
||||
|
||||
this.sendAction('setCodeMirror', this);
|
||||
|
||||
if (this.get('focus') && this.get('focusCursorAtEnd')) {
|
||||
codemirror.execCommand('goDocEnd');
|
||||
}
|
||||
|
||||
codemirror.eachLine(function initMarkers() {
|
||||
self.initMarkers.apply(self, arguments);
|
||||
});
|
||||
},
|
||||
|
||||
// this needs to be placed on the 'afterRender' queue otherwise CodeMirror gets wonky
|
||||
initCodemirror: function () {
|
||||
// create codemirror
|
||||
var codemirror,
|
||||
self = this;
|
||||
|
||||
codemirror = CodeMirror.fromTextArea(this.get('element'), {
|
||||
mode: 'gfm',
|
||||
tabMode: 'indent',
|
||||
tabindex: '2',
|
||||
cursorScrollMargin: 10,
|
||||
lineWrapping: true,
|
||||
dragDrop: false,
|
||||
extraKeys: {
|
||||
Home: 'goLineLeft',
|
||||
End: 'goLineRight',
|
||||
'Ctrl-U': false,
|
||||
'Cmd-U': false,
|
||||
'Shift-Ctrl-U': false,
|
||||
'Shift-Cmd-U': false,
|
||||
'Ctrl-S': false,
|
||||
'Cmd-S': false,
|
||||
'Ctrl-D': false,
|
||||
'Cmd-D': false
|
||||
}
|
||||
});
|
||||
|
||||
// Codemirror needs a reference to the component
|
||||
// so that codemirror originating events can propogate
|
||||
// up the ember action pipeline
|
||||
codemirror.component = this;
|
||||
|
||||
// propagate changes to value property
|
||||
codemirror.on('change', onChangeHandler);
|
||||
|
||||
// on scroll update scrollPosition property
|
||||
codemirror.on('scroll', onScrollHandler);
|
||||
|
||||
codemirror.on('scroll', Ember.run.bind(Ember.$('.CodeMirror-scroll'), setScrollClassName, {
|
||||
target: Ember.$('.js-entry-markdown'),
|
||||
offset: 10
|
||||
}));
|
||||
|
||||
codemirror.on('focus', function () {
|
||||
self.sendAction('onFocusIn');
|
||||
});
|
||||
|
||||
return 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);
|
||||
}.on('willDestroyElement'),
|
||||
|
||||
removeCodemirrorHandlers: function () {
|
||||
// not sure if this is needed.
|
||||
var codemirror = this.get('codemirror');
|
||||
codemirror.off('change', onChangeHandler);
|
||||
codemirror.off('scroll');
|
||||
}.on('willDestroyElement'),
|
||||
|
||||
clearMarkerManagerMarkers: function () {
|
||||
this.clearMarkers();
|
||||
}.on('willDestroyElement')
|
||||
});
|
||||
|
||||
export default Codemirror;
|
90
app/components/gh-ed-editor.js
Normal file
90
app/components/gh-ed-editor.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import Ember from 'ember';
|
||||
import EditorAPI from 'ghost/mixins/ed-editor-api';
|
||||
import EditorShortcuts from 'ghost/mixins/ed-editor-shortcuts';
|
||||
import EditorScroll from 'ghost/mixins/ed-editor-scroll';
|
||||
|
||||
var Editor;
|
||||
|
||||
Editor = Ember.TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, {
|
||||
focus: true,
|
||||
|
||||
/**
|
||||
* Tell the controller about focusIn events, will trigger an autosave on a new document
|
||||
*/
|
||||
focusIn: function () {
|
||||
this.sendAction('onFocusIn');
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the textarea should have focus, and set it if necessary
|
||||
*/
|
||||
setFocus: function () {
|
||||
if (this.get('focus')) {
|
||||
this.$().val(this.$().val()).focus();
|
||||
}
|
||||
}.on('didInsertElement'),
|
||||
|
||||
/**
|
||||
* Tell the controller about this component
|
||||
*/
|
||||
didInsertElement: function () {
|
||||
this.sendAction('setEditor', this);
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, this.afterRenderEvent);
|
||||
},
|
||||
|
||||
afterRenderEvent: function () {
|
||||
if (this.get('focus') && this.get('focusCursorAtEnd')) {
|
||||
this.setSelection('end');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Use keypress events to trigger autosave
|
||||
*/
|
||||
changeHandler: function () {
|
||||
// onChange is sent to trigger autosave
|
||||
this.sendAction('onChange');
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind to the keypress event once the element is in the DOM
|
||||
* Use keypress because it's the most reliable cross browser
|
||||
*/
|
||||
attachChangeHandler: function () {
|
||||
this.$().on('keypress', Ember.run.bind(this, this.changeHandler));
|
||||
}.on('didInsertElement'),
|
||||
|
||||
/**
|
||||
* Unbind from the keypress event when the element is no longer in the DOM
|
||||
*/
|
||||
detachChangeHandler: function () {
|
||||
this.$().off('keypress');
|
||||
Ember.run.cancel(this.get('fixHeightThrottle'));
|
||||
}.on('willDestroyElement'),
|
||||
|
||||
/**
|
||||
* Disable editing in the textarea (used while an upload is in progress)
|
||||
*/
|
||||
disable: function () {
|
||||
var textarea = this.get('element');
|
||||
textarea.setAttribute('readonly', 'readonly');
|
||||
|
||||
this.detachChangeHandler();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reenable editing in the textarea
|
||||
*/
|
||||
enable: function () {
|
||||
var textarea = this.get('element');
|
||||
textarea.removeAttribute('readonly');
|
||||
|
||||
// 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.
|
||||
this.detachChangeHandler();
|
||||
this.attachChangeHandler();
|
||||
}
|
||||
});
|
||||
|
||||
export default Editor;
|
41
app/components/gh-ed-preview.js
Normal file
41
app/components/gh-ed-preview.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import Ember from 'ember';
|
||||
import uploader from 'ghost/assets/lib/uploader';
|
||||
|
||||
var Preview = Ember.Component.extend({
|
||||
didInsertElement: function () {
|
||||
this.set('scrollWrapper', this.$().closest('.entry-preview-content'));
|
||||
Ember.run.scheduleOnce('afterRender', this, this.dropzoneHandler);
|
||||
},
|
||||
|
||||
adjustScrollPosition: function () {
|
||||
var scrollWrapper = this.get('scrollWrapper'),
|
||||
scrollPosition = this.get('scrollPosition');
|
||||
|
||||
scrollWrapper.scrollTop(scrollPosition);
|
||||
}.observes('scrollPosition'),
|
||||
|
||||
dropzoneHandler: function () {
|
||||
var dropzones = $('.js-drop-zone');
|
||||
|
||||
uploader.call(dropzones, {
|
||||
editor: true,
|
||||
fileStorage: this.get('config.fileStorage')
|
||||
});
|
||||
|
||||
dropzones.on('uploadstart', Ember.run.bind(this, 'sendAction', 'uploadStarted'));
|
||||
dropzones.on('uploadfailure', Ember.run.bind(this, 'sendAction', 'uploadFinished'));
|
||||
dropzones.on('uploadsuccess', Ember.run.bind(this, 'sendAction', 'uploadFinished'));
|
||||
dropzones.on('uploadsuccess', Ember.run.bind(this, 'sendAction', 'uploadSuccess'));
|
||||
|
||||
// Set the current height so we can listen
|
||||
this.set('height', this.$().height());
|
||||
},
|
||||
|
||||
// fire off 'enable' API function from uploadManager
|
||||
// might need to make sure markdown has been processed first
|
||||
reInitDropzones: function () {
|
||||
Ember.run.scheduleOnce('afterRender', this, this.dropzoneHandler);
|
||||
}.observes('markdown')
|
||||
});
|
||||
|
||||
export default Preview;
|
|
@ -1,37 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
import uploader from 'ghost/assets/lib/uploader';
|
||||
|
||||
var Markdown = Ember.Component.extend({
|
||||
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 () {
|
||||
function handleDropzoneEvents() {
|
||||
var dropzones = $('.js-drop-zone');
|
||||
|
||||
uploader.call(dropzones, {
|
||||
editor: true,
|
||||
fileStorage: this.get('config.fileStorage')
|
||||
});
|
||||
|
||||
dropzones.on('uploadstart', Ember.run.bind(this, 'sendAction', 'uploadStarted'));
|
||||
dropzones.on('uploadfailure', Ember.run.bind(this, 'sendAction', 'uploadFinished'));
|
||||
dropzones.on('uploadsuccess', Ember.run.bind(this, 'sendAction', 'uploadFinished'));
|
||||
dropzones.on('uploadsuccess', Ember.run.bind(this, 'sendAction', 'uploadSuccess'));
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, handleDropzoneEvents);
|
||||
}.observes('markdown')
|
||||
});
|
||||
|
||||
export default Markdown;
|
136
app/mixins/ed-editor-api.js
Normal file
136
app/mixins/ed-editor-api.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
var EditorAPI = Ember.Mixin.create({
|
||||
/**
|
||||
* Get Value
|
||||
*
|
||||
* Get the full contents of the textarea
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
getValue: function () {
|
||||
return this.$().val();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Selection
|
||||
*
|
||||
* Return the currently selected text from the textarea
|
||||
*
|
||||
* @returns {Selection}
|
||||
*/
|
||||
getSelection: function () {
|
||||
return this.$().getSelection();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Line To Cursor
|
||||
*
|
||||
* Fetch the string of characters from the start of the given line up to the cursor
|
||||
* @returns {{text: string, start: number}}
|
||||
*/
|
||||
getLineToCursor: function () {
|
||||
var selection = this.$().getSelection(),
|
||||
value = this.getValue(),
|
||||
lineStart;
|
||||
|
||||
// Normalise newlines
|
||||
value = value.replace('\r\n', '\n');
|
||||
|
||||
// We want to look at the characters behind the cursor
|
||||
lineStart = value.lastIndexOf('\n', selection.start - 1) + 1;
|
||||
|
||||
return {
|
||||
text: value.substring(lineStart, selection.start),
|
||||
start: lineStart
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Line
|
||||
*
|
||||
* Return the string of characters for the line the cursor is currently on
|
||||
*
|
||||
* @returns {{text: string, start: number, end: number}}
|
||||
*/
|
||||
getLine: function () {
|
||||
var selection = this.$().getSelection(),
|
||||
value = this.getValue(),
|
||||
lineStart,
|
||||
lineEnd;
|
||||
|
||||
// Normalise newlines
|
||||
value = value.replace('\r\n', '\n');
|
||||
|
||||
// We want to look at the characters behind the cursor
|
||||
lineStart = value.lastIndexOf('\n', selection.start - 1) + 1;
|
||||
lineEnd = value.indexOf('\n', selection.start);
|
||||
lineEnd = lineEnd === -1 ? value.length - 1 : lineEnd;
|
||||
|
||||
return {
|
||||
// jscs:disable
|
||||
text: value.substring(lineStart, lineEnd).replace(/^\n/, ''),
|
||||
// jscs:enable
|
||||
start: lineStart,
|
||||
end: lineEnd
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Selection
|
||||
*
|
||||
* Set the section of text in the textarea that should be selected by the cursor
|
||||
*
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
*/
|
||||
setSelection: function (start, end) {
|
||||
var $textarea = this.$();
|
||||
|
||||
if (start === 'end') {
|
||||
start = $textarea.val().length;
|
||||
}
|
||||
|
||||
end = end || start;
|
||||
|
||||
$textarea.setSelection(start, end);
|
||||
},
|
||||
|
||||
/**
|
||||
* Replace Selection
|
||||
*
|
||||
* @param {String} replacement - the string to replace with
|
||||
* @param {number} replacementStart - where to start replacing
|
||||
* @param {number} [replacementEnd] - when to stop replacing, defaults to replacementStart
|
||||
* @param {String|boolean|Object} [cursorPosition] - where to put the cursor after replacing
|
||||
*
|
||||
* Cursor position after replacement defaults to the end of the replacement.
|
||||
* Providing selectionStart only will cause the cursor to be placed there, or alternatively a range can be selected
|
||||
* by providing selectionEnd.
|
||||
*/
|
||||
replaceSelection: function (replacement, replacementStart, replacementEnd, cursorPosition) {
|
||||
var $textarea = this.$();
|
||||
|
||||
cursorPosition = cursorPosition || 'collapseToEnd';
|
||||
replacementEnd = replacementEnd || replacementStart;
|
||||
|
||||
$textarea.setSelection(replacementStart, replacementEnd);
|
||||
|
||||
if (['select', 'collapseToStart', 'collapseToEnd'].indexOf(cursorPosition) !== -1) {
|
||||
$textarea.replaceSelectedText(replacement, cursorPosition);
|
||||
} else {
|
||||
$textarea.replaceSelectedText(replacement);
|
||||
if (cursorPosition.hasOwnProperty('start')) {
|
||||
$textarea.setSelection(cursorPosition.start, cursorPosition.end);
|
||||
} else {
|
||||
$textarea.setSelection(cursorPosition, cursorPosition);
|
||||
}
|
||||
}
|
||||
|
||||
$textarea.focus();
|
||||
// Tell the editor it has changed, as programmatic replacements won't trigger this automatically
|
||||
this.sendAction('onChange');
|
||||
}
|
||||
});
|
||||
|
||||
export default EditorAPI;
|
94
app/mixins/ed-editor-scroll.js
Normal file
94
app/mixins/ed-editor-scroll.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
import Ember from 'ember';
|
||||
|
||||
import setScrollClassName from 'ghost/utils/set-scroll-classname';
|
||||
|
||||
var EditorScroll = Ember.Mixin.create({
|
||||
/**
|
||||
* Determine if the cursor is at the end of the textarea
|
||||
*/
|
||||
isCursorAtEnd: function () {
|
||||
var selection = this.$().getSelection(),
|
||||
value = this.getValue(),
|
||||
linesAtEnd = 3,
|
||||
stringAfterCursor,
|
||||
match;
|
||||
|
||||
stringAfterCursor = value.substring(selection.end);
|
||||
/* jscs: disable */
|
||||
match = stringAfterCursor.match(/\n/g);
|
||||
/* jscs: enable */
|
||||
|
||||
if (!match || match.length < linesAtEnd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Build an object that represents the scroll state
|
||||
*/
|
||||
getScrollInfo: function () {
|
||||
var scroller = this.get('element'),
|
||||
scrollInfo = {
|
||||
top: scroller.scrollTop,
|
||||
height: scroller.scrollHeight,
|
||||
clientHeight: scroller.clientHeight,
|
||||
diff: scroller.scrollHeight - scroller.clientHeight,
|
||||
padding: 50,
|
||||
isCursorAtEnd: this.isCursorAtEnd()
|
||||
};
|
||||
|
||||
return scrollInfo;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate if we're within scrollInfo.padding of the end of the document, and scroll the rest of the way
|
||||
*/
|
||||
adjustScrollPosition: function () {
|
||||
// If we're receiving change events from the end of the document, i.e the user is typing-at-the-end, update the
|
||||
// scroll position to ensure both panels stay in view and in sync
|
||||
var scrollInfo = this.getScrollInfo();
|
||||
if (scrollInfo.isCursorAtEnd && (scrollInfo.diff >= scrollInfo.top) &&
|
||||
(scrollInfo.diff < scrollInfo.top + scrollInfo.padding)) {
|
||||
scrollInfo.top += scrollInfo.padding;
|
||||
// Scroll the left pane
|
||||
this.$().scrollTop(scrollInfo.top);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send the scrollInfo for scrollEvents to the view so that the preview pane can be synced
|
||||
*/
|
||||
scrollHandler: function () {
|
||||
this.set('scrollThrottle', Ember.run.throttle(this, function () {
|
||||
this.set('scrollInfo', this.getScrollInfo());
|
||||
}, 10));
|
||||
},
|
||||
|
||||
/**
|
||||
* once the element is in the DOM bind to the events which control scroll behaviour
|
||||
*/
|
||||
attachScrollHandlers: function () {
|
||||
var $el = this.$();
|
||||
|
||||
$el.on('keypress', Ember.run.bind(this, this.adjustScrollPosition));
|
||||
|
||||
$el.on('scroll', Ember.run.bind(this, this.scrollHandler));
|
||||
$el.on('scroll', Ember.run.bind($el, setScrollClassName, {
|
||||
target: Ember.$('.js-entry-markdown'),
|
||||
offset: 10
|
||||
}));
|
||||
}.on('didInsertElement'),
|
||||
|
||||
/**
|
||||
* once the element is in the DOM unbind from the events which control scroll behaviour
|
||||
*/
|
||||
detachScrollHandlers: function () {
|
||||
this.$().off('keypress');
|
||||
this.$().off('scroll');
|
||||
Ember.run.cancel(this.get('scrollThrottle'));
|
||||
}.on('willDestroyElement')
|
||||
});
|
||||
|
||||
export default EditorScroll;
|
175
app/mixins/ed-editor-shortcuts.js
Normal file
175
app/mixins/ed-editor-shortcuts.js
Normal file
|
@ -0,0 +1,175 @@
|
|||
/* global moment, Showdown */
|
||||
import Ember from 'ember';
|
||||
import titleize from 'ghost/utils/titleize';
|
||||
|
||||
var simpleShortcutSyntax,
|
||||
shortcuts,
|
||||
EditorShortcuts;
|
||||
|
||||
// Used for simple, noncomputational replace-and-go! shortcuts.
|
||||
// See default case in shortcut function below.
|
||||
simpleShortcutSyntax = {
|
||||
bold: {
|
||||
regex: '**|**',
|
||||
cursor: '|'
|
||||
},
|
||||
italic: {
|
||||
regex: '*|*',
|
||||
cursor: '|'
|
||||
|
||||
},
|
||||
strike: {
|
||||
regex: '~~|~~',
|
||||
cursor: '|'
|
||||
},
|
||||
code: {
|
||||
regex: '`|`',
|
||||
cursor: '|'
|
||||
},
|
||||
blockquote: {
|
||||
regex: '> |',
|
||||
cursor: '|',
|
||||
newline: true
|
||||
},
|
||||
list: {
|
||||
regex: '* |',
|
||||
cursor: '|',
|
||||
newline: true
|
||||
},
|
||||
link: {
|
||||
regex: '[|](http://)',
|
||||
cursor: 'http://'
|
||||
},
|
||||
image: {
|
||||
regex: '![|](http://)',
|
||||
cursor: 'http://',
|
||||
newline: true
|
||||
}
|
||||
};
|
||||
|
||||
shortcuts = {
|
||||
simple: function (type, replacement, selection, line) {
|
||||
var shortcut,
|
||||
startIndex = 0;
|
||||
|
||||
if (simpleShortcutSyntax.hasOwnProperty(type)) {
|
||||
shortcut = simpleShortcutSyntax[type];
|
||||
// insert the markdown
|
||||
replacement.text = shortcut.regex.replace('|', selection.text);
|
||||
|
||||
// add a newline if needed
|
||||
if (shortcut.newline && line.text !== '') {
|
||||
startIndex = 1;
|
||||
replacement.text = '\n' + replacement.text;
|
||||
}
|
||||
|
||||
// handle cursor position
|
||||
if (selection.text === '' && shortcut.cursor === '|') {
|
||||
// the cursor should go where | was
|
||||
replacement.position = startIndex + replacement.start + shortcut.regex.indexOf(shortcut.cursor);
|
||||
} else if (shortcut.cursor !== '|') {
|
||||
// the cursor should select the string which matches shortcut.cursor
|
||||
replacement.position = {
|
||||
start: replacement.start + replacement.text.indexOf(shortcut.cursor)
|
||||
};
|
||||
replacement.position.end = replacement.position.start + shortcut.cursor.length;
|
||||
}
|
||||
}
|
||||
|
||||
return replacement;
|
||||
},
|
||||
cycleHeaderLevel: function (replacement, line) {
|
||||
// jscs:disable
|
||||
var match = line.text.match(/^#+/),
|
||||
// jscs:enable
|
||||
currentHeaderLevel,
|
||||
hashPrefix;
|
||||
|
||||
if (!match) {
|
||||
currentHeaderLevel = 1;
|
||||
} else {
|
||||
currentHeaderLevel = match[0].length;
|
||||
}
|
||||
|
||||
if (currentHeaderLevel > 2) {
|
||||
currentHeaderLevel = 1;
|
||||
}
|
||||
|
||||
hashPrefix = new Array(currentHeaderLevel + 2).join('#');
|
||||
|
||||
// jscs:disable
|
||||
replacement.text = hashPrefix + ' ' + line.text.replace(/^#* /, '');
|
||||
// jscs:enable
|
||||
|
||||
replacement.start = line.start;
|
||||
replacement.end = line.end;
|
||||
|
||||
return replacement;
|
||||
},
|
||||
copyHTML: function (editor, selection) {
|
||||
var converter = new Showdown.converter(),
|
||||
generatedHTML;
|
||||
|
||||
if (selection.text) {
|
||||
generatedHTML = converter.makeHtml(selection.text);
|
||||
} else {
|
||||
generatedHTML = converter.makeHtml(editor.getValue());
|
||||
}
|
||||
|
||||
// Talk to the editor
|
||||
editor.sendAction('openModal', 'copy-html', {generatedHTML: generatedHTML});
|
||||
},
|
||||
currentDate: function (replacement) {
|
||||
replacement.text = moment(new Date()).format('D MMMM YYYY');
|
||||
return replacement;
|
||||
},
|
||||
uppercase: function (replacement, selection) {
|
||||
replacement.text = selection.text.toLocaleUpperCase();
|
||||
return replacement;
|
||||
},
|
||||
lowercase: function (replacement, selection) {
|
||||
replacement.text = selection.text.toLocaleLowerCase();
|
||||
return replacement;
|
||||
},
|
||||
titlecase: function (replacement, selection) {
|
||||
replacement.text = titleize(selection.text);
|
||||
return replacement;
|
||||
}
|
||||
};
|
||||
|
||||
EditorShortcuts = Ember.Mixin.create({
|
||||
shortcut: function (type) {
|
||||
var selection = this.getSelection(),
|
||||
replacement = {
|
||||
start: selection.start,
|
||||
end: selection.end,
|
||||
position: 'collapseToEnd'
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
// This shortcut is special as it needs to send an action
|
||||
case 'copyHTML':
|
||||
shortcuts.copyHTML(this, selection);
|
||||
break;
|
||||
case 'cycleHeaderLevel':
|
||||
replacement = shortcuts.cycleHeaderLevel(replacement, this.getLine());
|
||||
break;
|
||||
// These shortcuts all process the basic information
|
||||
case 'currentDate':
|
||||
case 'uppercase':
|
||||
case 'lowercase':
|
||||
case 'titlecase':
|
||||
replacement = shortcuts[type](replacement, selection, this.getLineToCursor());
|
||||
break;
|
||||
// All the of basic formatting shortcuts work with a regex
|
||||
default:
|
||||
replacement = shortcuts.simple(type, replacement, selection, this.getLineToCursor());
|
||||
}
|
||||
|
||||
if (replacement.text) {
|
||||
this.replaceSelection(replacement.text, replacement.start, replacement.end, replacement.position);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default EditorShortcuts;
|
|
@ -1,8 +1,8 @@
|
|||
import Ember from 'ember';
|
||||
/* global console */
|
||||
import MarkerManager from 'ghost/mixins/marker-manager';
|
||||
import PostModel from 'ghost/models/post';
|
||||
import boundOneWay from 'ghost/utils/bound-one-way';
|
||||
import imageManager from 'ghost/utils/ed-image-manager';
|
||||
|
||||
var watchedProps,
|
||||
EditorControllerMixin;
|
||||
|
@ -15,13 +15,12 @@ PostModel.eachAttribute(function (name) {
|
|||
watchedProps.push('model.' + name);
|
||||
});
|
||||
|
||||
EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
|
||||
EditorControllerMixin = Ember.Mixin.create({
|
||||
needs: ['post-tags-input', 'post-settings-menu'],
|
||||
|
||||
autoSaveId: null,
|
||||
timedSaveId: null,
|
||||
codemirror: null,
|
||||
codemirrorComponent: null,
|
||||
editor: null,
|
||||
|
||||
init: function () {
|
||||
var self = this;
|
||||
|
@ -109,7 +108,7 @@ EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
|
|||
markdown = model.get('markdown'),
|
||||
title = model.get('title'),
|
||||
titleScratch = model.get('titleScratch'),
|
||||
scratch = this.getMarkdown().withoutMarkers,
|
||||
scratch = this.get('editor').getValue(),
|
||||
changedAttributes;
|
||||
|
||||
if (!this.tagNamesEqual()) {
|
||||
|
@ -243,7 +242,7 @@ EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
|
|||
|
||||
// Set the properties that are indirected
|
||||
// set markdown equal to what's in the editor, minus the image markers.
|
||||
this.set('model.markdown', this.getMarkdown().withoutMarkers);
|
||||
this.set('model.markdown', this.get('editor').getValue());
|
||||
this.set('model.status', status);
|
||||
|
||||
// Set a default title
|
||||
|
@ -296,56 +295,37 @@ EditorControllerMixin = Ember.Mixin.create(MarkerManager, {
|
|||
}
|
||||
},
|
||||
|
||||
// set from a `sendAction` on the codemirror component,
|
||||
// set from a `sendAction` on the gh-ed-editor 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);
|
||||
setEditor: function (editor) {
|
||||
this.set('editor', editor);
|
||||
},
|
||||
|
||||
// fired from the gh-markdown component when an image upload starts
|
||||
disableCodeMirror: function () {
|
||||
this.get('codemirrorComponent').disableCodeMirror();
|
||||
// fired from the gh-ed-preview component when an image upload starts
|
||||
disableEditor: function () {
|
||||
this.get('editor').disable();
|
||||
},
|
||||
|
||||
// fired from the gh-markdown component when an image upload finishes
|
||||
enableCodeMirror: function () {
|
||||
this.get('codemirrorComponent').enableCodeMirror();
|
||||
// fired from the gh-ed-preview component when an image upload finishes
|
||||
enableEditor: function () {
|
||||
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: function (e, resultSrc) {
|
||||
var editor = this.get('codemirror'),
|
||||
line = this.findLine(Ember.$(e.currentTarget).attr('id')),
|
||||
lineNumber = editor.getLineNumber(line),
|
||||
match = line.text.match(/\([^\n]*\)?/),
|
||||
replacement = '(http://)';
|
||||
var editor = this.get('editor'),
|
||||
editorValue = editor.getValue(),
|
||||
replacement = imageManager.getSrcRange(editorValue, e.target),
|
||||
cursorPosition;
|
||||
|
||||
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}
|
||||
);
|
||||
if (replacement) {
|
||||
cursorPosition = replacement.start + resultSrc.length + 1;
|
||||
if (replacement.needsParens) {
|
||||
resultSrc = '(' + resultSrc + ')';
|
||||
}
|
||||
editor.replaceSelection(resultSrc, replacement.start, replacement.end, cursorPosition);
|
||||
}
|
||||
|
||||
editor.replaceSelection(resultSrc);
|
||||
},
|
||||
|
||||
togglePreview: function (preview) {
|
||||
|
|
|
@ -23,11 +23,11 @@ var EditorBaseRoute = Ember.Mixin.create(styleBody, ShortcutsRoute, loadingIndic
|
|||
Ember.$('body').toggleClass('zen');
|
||||
},
|
||||
|
||||
// The actual functionality is implemented in utils/codemirror-shortcuts
|
||||
codeMirrorShortcut: function (options) {
|
||||
// The actual functionality is implemented in utils/ed-editor-shortcuts
|
||||
editorShortcut: function (options) {
|
||||
// Only fire editor shortcuts when the editor has focus.
|
||||
if (Ember.$('.CodeMirror.CodeMirror-focused').length > 0) {
|
||||
this.get('controller.codemirror').shortcut(options.type);
|
||||
if (this.get('controller.editor').$().is(':focus')) {
|
||||
this.get('controller.editor').shortcut(options.type);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ var EditorViewMixin = Ember.Mixin.create({
|
|||
this.set('$previewViewPort', $previewViewPort);
|
||||
this.set('$previewContent', this.$('.js-rendered-markdown'));
|
||||
|
||||
$previewViewPort.scroll(Ember.run.bind($previewViewPort, setScrollClassName, {
|
||||
$previewViewPort.on('scroll', Ember.run.bind($previewViewPort, setScrollClassName, {
|
||||
target: this.$('.js-entry-preview'),
|
||||
offset: 10
|
||||
}));
|
||||
|
@ -31,26 +31,29 @@ var EditorViewMixin = Ember.Mixin.create({
|
|||
this.get('$previewViewPort').off('scroll');
|
||||
}.on('willDestroyElement'),
|
||||
|
||||
// updated when gh-codemirror component scrolls
|
||||
markdownScrollInfo: null,
|
||||
// updated when gh-ed-editor component scrolls
|
||||
editorScrollInfo: null,
|
||||
// updated when markdown is rendered
|
||||
height: null,
|
||||
|
||||
// percentage of scroll position to set htmlPreview
|
||||
scrollPosition: Ember.computed('markdownScrollInfo', function () {
|
||||
if (!this.get('markdownScrollInfo')) {
|
||||
// HTML Preview listens to scrollPosition and updates its scrollTop value
|
||||
// This property receives scrollInfo from the textEditor, and height from the preview pane, and will update the
|
||||
// scrollPosition value such that when either scrolling or typing-at-the-end of the text editor the preview pane
|
||||
// stays in sync
|
||||
scrollPosition: Ember.computed('editorScrollInfo', 'height', function () {
|
||||
if (!this.get('editorScrollInfo')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var scrollInfo = this.get('markdownScrollInfo'),
|
||||
markdownHeight,
|
||||
previewHeight,
|
||||
var scrollInfo = this.get('editorScrollInfo'),
|
||||
previewHeight = this.get('$previewContent').height() - this.get('$previewViewPort').height(),
|
||||
previewPosition,
|
||||
ratio;
|
||||
|
||||
markdownHeight = scrollInfo.height - scrollInfo.clientHeight;
|
||||
previewHeight = this.get('$previewContent').height() - this.get('$previewViewPort').height();
|
||||
ratio = previewHeight / scrollInfo.diff;
|
||||
previewPosition = scrollInfo.top * ratio;
|
||||
|
||||
ratio = previewHeight / markdownHeight;
|
||||
|
||||
return scrollInfo.top * ratio;
|
||||
return previewPosition;
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -1,220 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
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 editor = this.get('codemirror'),
|
||||
ln = editor.getLineNumber(line),
|
||||
|
||||
markerRegex = /\{<([\w\W]*?)>\}/,
|
||||
|
||||
markerText = line.text.match(markerRegex);
|
||||
|
||||
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 (resultId) {
|
||||
var editor = this.get('codemirror'),
|
||||
markers = this.get('markers');
|
||||
|
||||
// try to find the right line to replace
|
||||
if (markers.hasOwnProperty(resultId) && markers[resultId].find()) {
|
||||
return editor.getLineHandle(markers[resultId].find().from.line);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
export default MarkerManager;
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
@import "../../../../bower_components/normalize-scss/_normalize"; // via Bower
|
||||
@import "vendor/nprogress";
|
||||
@import "vendor/codemirror";
|
||||
@import "vendor/nanoscroller";
|
||||
|
||||
|
||||
|
|
|
@ -181,10 +181,10 @@
|
|||
border: 0;
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
height: 100%;
|
||||
height: calc(100% - 40px);
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 10px 20px 50px 20px;
|
||||
padding: 22px 20px 35px 20px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
@ -192,10 +192,21 @@
|
|||
left: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
font-size: 2rem;
|
||||
line-height: 1.7em;
|
||||
font-family: $font-family-mono;
|
||||
color: lighten($darkgrey, 10%);
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: $darkgrey;
|
||||
background: lighten($blue, 20%);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
@ -211,77 +222,8 @@
|
|||
@media (max-height: 560px) {
|
||||
height: calc(100% - 5px);
|
||||
}
|
||||
}//textarea
|
||||
}//.entry-markdown-content
|
||||
|
||||
.CodeMirror {
|
||||
height: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
pre {
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.56em;
|
||||
font-family: $font-family-mono;
|
||||
color: lighten($darkgrey, 10%);
|
||||
}
|
||||
|
||||
.CodeMirror-focused,
|
||||
.CodeMirror-selected {
|
||||
color: $darkgrey;
|
||||
background: lighten($blue, 20%);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: $darkgrey;
|
||||
background: lighten($blue, 20%);
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding-top: 65px; /* Vertical padding around content */
|
||||
@media (max-width: 1000px) {padding-top: 25px;}
|
||||
@media (max-width: 400px) {padding: 15px 0;}
|
||||
}
|
||||
|
||||
.CodeMirror pre {
|
||||
padding: 0 40px; /* Horizontal padding of content */
|
||||
@media (max-width: 400px) {padding: 0 15px;}
|
||||
}
|
||||
|
||||
.cm-header {
|
||||
color: #3c4043;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.4em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cm-variable-2,
|
||||
.cm-variable-3,
|
||||
.cm-keyword {
|
||||
color: lighten($darkgrey, 10%);
|
||||
}
|
||||
|
||||
|
||||
.cm-strong,
|
||||
.cm-comment,
|
||||
.cm-quote,
|
||||
.cm-number,
|
||||
.cm-atom,
|
||||
.cm-tag {
|
||||
color: #3c4043;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cm-string,
|
||||
.cm-link {
|
||||
color: #3c4043;
|
||||
}
|
||||
} // textarea
|
||||
} // .entry-markdown-content
|
||||
|
||||
.entry-preview {
|
||||
// Align the tab of entry-preview on the right
|
||||
|
@ -319,7 +261,7 @@
|
|||
|
||||
// Special case, when scrolling, add shadows to content headers.
|
||||
@media (max-width: 1000px) {
|
||||
.scrolling{
|
||||
.scrolling {
|
||||
.floatingheader {
|
||||
box-shadow: none;
|
||||
|
||||
|
@ -328,10 +270,11 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.entry-preview-content {
|
||||
@media (max-width: 1000px) {
|
||||
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.05) inset;
|
||||
|
||||
.CodeMirror-scroll,
|
||||
.entry-preview-content {
|
||||
box-shadow: 0 5px 5px rgba(0,0,0,0.05) inset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
207
app/styles/vendor/codemirror.scss
vendored
207
app/styles/vendor/codemirror.scss
vendored
|
@ -1,207 +0,0 @@
|
|||
//
|
||||
// CodeMirror
|
||||
// --------------------------------------------------
|
||||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
/* Set scrolling behaviour here */
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
}
|
||||
|
||||
.CodeMirror pre {
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler {
|
||||
background-color: white; /* The little square between H and V scrollbars */
|
||||
}
|
||||
|
||||
/* GUTTER */
|
||||
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
/* CURSOR */
|
||||
|
||||
.CodeMirror div.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
z-index: 3;
|
||||
}
|
||||
/* Shown when moving in bi-directional text */
|
||||
.CodeMirror div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
|
||||
|
||||
.cm-tab { display: inline-block; }
|
||||
|
||||
/* DEFAULT THEME */
|
||||
|
||||
.cm-s-default .cm-keyword {color: #708;}
|
||||
.cm-s-default .cm-atom {color: #219;}
|
||||
.cm-s-default .cm-number {color: #164;}
|
||||
.cm-s-default .cm-def {color: #00f;}
|
||||
.cm-s-default .cm-variable {color: #3c4043;}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3 {color: #085;}
|
||||
.cm-s-default .cm-property {color: #3c4043;}
|
||||
.cm-s-default .cm-operator {color: #3c4043;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
.cm-s-default .cm-meta {color: #555;}
|
||||
.cm-s-default .cm-error {color: #3c4043;}
|
||||
.cm-s-default .cm-qualifier {color: #555;}
|
||||
.cm-s-default .cm-builtin {color: #30a;}
|
||||
.cm-s-default .cm-bracket {color: #997;}
|
||||
.cm-s-default .cm-tag {color: #170;}
|
||||
.cm-s-default .cm-attribute {color: #00c;}
|
||||
.cm-s-default .cm-header {color: blue;}
|
||||
.cm-s-default .cm-quote {color: #090;}
|
||||
.cm-s-default .cm-hr {color: #999;}
|
||||
.cm-s-default .cm-link {color: #00c;}
|
||||
|
||||
.cm-negative {color: #d44;}
|
||||
.cm-positive {color: #292;}
|
||||
.cm-header, .cm-strong {font-weight: bold;}
|
||||
.cm-em {font-style: italic;}
|
||||
.cm-link {text-decoration: underline;}
|
||||
|
||||
.cm-invalidchar {color: #f00;}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/* The rest of this file contains styles related to the mechanics of
|
||||
the editor. You probably shouldn't touch them. */
|
||||
|
||||
.CodeMirror {
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
/* 30px is the magic margin used to hide the element's real scrollbars */
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-right: -30px; padding-right: 30px;
|
||||
height: 100%;
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
position: relative;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
before actuall scrolling happens, thus preventing shaking and
|
||||
flickering artifacts. */
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
right: 0; top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 0; left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler {
|
||||
right: 0; bottom: 0;
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
position: absolute; left: 0; top: 0;
|
||||
height: 100%;
|
||||
padding-bottom: 30px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.CodeMirror pre {
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-wrap pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.CodeMirror-wrap .CodeMirror-scroll {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%; height: 0px;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-measure pre { position: static; }
|
||||
|
||||
.CodeMirror:not(.CodeMirror-focused) {
|
||||
div.CodeMirror-cursor {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror div.CodeMirror-cursor {
|
||||
position: absolute;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-selected { background: #d9d9d9; }
|
||||
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||
|
||||
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
|
||||
.CodeMirror span { *vertical-align: text-bottom; }
|
||||
|
||||
@media print {
|
||||
/* Hide the cursor when printing */
|
||||
.CodeMirror div.CodeMirror-cursor {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,9 @@
|
|||
<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=model.scratch scrollInfo=view.markdownScrollInfo
|
||||
setCodeMirror="setCodeMirror" openModal="openModal" typingPause="autoSave"
|
||||
{{gh-ed-editor classNames="markdown-editor js-markdown-editor" tabindex="1" spellcheck="true" value=model.scratch
|
||||
scrollInfo=view.editorScrollInfo
|
||||
setEditor="setEditor" openModal="openModal" onChange="autoSave"
|
||||
focus=shouldFocusEditor focusCursorAtEnd=model.isDirty onFocusIn="autoSaveNew"}}
|
||||
</section>
|
||||
</section>
|
||||
|
@ -28,9 +29,9 @@
|
|||
<small>Preview <span class="entry-word-count js-entry-word-count">{{gh-count-words model.scratch}}</span></small>
|
||||
</header>
|
||||
<section class="entry-preview-content js-entry-preview-content">
|
||||
{{gh-markdown classNames="rendered-markdown js-rendered-markdown"
|
||||
markdown=model.scratch scrollPosition=view.scrollPosition
|
||||
uploadStarted="disableCodeMirror" uploadFinished="enableCodeMirror" uploadSuccess="handleImgUpload"}}
|
||||
{{gh-ed-preview classNames="rendered-markdown js-rendered-markdown"
|
||||
markdown=model.scratch scrollPosition=view.scrollPosition height=view.height
|
||||
uploadStarted="disableEditor" uploadFinished="enableEditor" uploadSuccess="handleImgUpload"}}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import Ember from 'ember';
|
||||
/*global CodeMirror, device, FastClick*/
|
||||
import createTouchEditor from 'ghost/assets/lib/touch-editor';
|
||||
|
||||
var setupMobileCodeMirror,
|
||||
TouchEditor,
|
||||
init;
|
||||
|
||||
setupMobileCodeMirror = function setupMobileCodeMirror() {
|
||||
var noop = function () {},
|
||||
key;
|
||||
|
||||
for (key in CodeMirror) {
|
||||
if (CodeMirror.hasOwnProperty(key)) {
|
||||
CodeMirror[key] = noop;
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.fromTextArea = function (el, options) {
|
||||
return new TouchEditor(el, options);
|
||||
};
|
||||
|
||||
CodeMirror.keyMap = {basic: {}};
|
||||
};
|
||||
|
||||
init = function init() {
|
||||
// Codemirror does not function on mobile devices, or on any iDevice
|
||||
if (device.mobile() || (device.tablet() && device.ios())) {
|
||||
$('body').addClass('touch-editor');
|
||||
|
||||
Ember.touchEditor = true;
|
||||
|
||||
// initialize FastClick to remove touch delays
|
||||
Ember.run.scheduleOnce('afterRender', null, function () {
|
||||
FastClick.attach(document.body);
|
||||
});
|
||||
|
||||
TouchEditor = createTouchEditor();
|
||||
setupMobileCodeMirror();
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createIfMobile: init
|
||||
};
|
|
@ -1,142 +0,0 @@
|
|||
/* global CodeMirror, moment, Showdown */
|
||||
// jscs:disable disallowSpacesInsideParentheses
|
||||
|
||||
/** Set up a shortcut function to be called via router actions.
|
||||
* See editor-base-route
|
||||
*/
|
||||
|
||||
import titleize from 'ghost/utils/titleize';
|
||||
|
||||
function init() {
|
||||
// remove predefined `ctrl+h` shortcut
|
||||
delete CodeMirror.keyMap.emacsy['Ctrl-H'];
|
||||
|
||||
// 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},
|
||||
toLineEnd = {line: cursor.line, ch: line.length},
|
||||
md, letterCount, textIndex, position, converter,
|
||||
generatedHTML, match, currentHeaderLevel, hashPrefix,
|
||||
replacementLine;
|
||||
|
||||
switch (type) {
|
||||
case 'cycleHeaderLevel':
|
||||
match = line.match(/^#+/);
|
||||
|
||||
if (!match) {
|
||||
currentHeaderLevel = 1;
|
||||
} else {
|
||||
currentHeaderLevel = match[0].length;
|
||||
}
|
||||
|
||||
if (currentHeaderLevel > 2) {
|
||||
currentHeaderLevel = 1;
|
||||
}
|
||||
|
||||
hashPrefix = new Array(currentHeaderLevel + 2).join('#');
|
||||
|
||||
replacementLine = hashPrefix + ' ' + line.replace(/^#* /, '');
|
||||
|
||||
this.replaceRange(replacementLine, fromLineStart, toLineEnd);
|
||||
this.setCursor(cursor.line, cursor.ch + replacementLine.length);
|
||||
break;
|
||||
|
||||
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;
|
||||
|
||||
case 'uppercase':
|
||||
md = text.toLocaleUpperCase();
|
||||
break;
|
||||
|
||||
case 'lowercase':
|
||||
md = text.toLocaleLowerCase();
|
||||
break;
|
||||
|
||||
case 'titlecase':
|
||||
md = titleize(text);
|
||||
break;
|
||||
|
||||
case 'copyHTML':
|
||||
converter = new Showdown.converter();
|
||||
|
||||
if (text) {
|
||||
generatedHTML = converter.makeHtml(text);
|
||||
} else {
|
||||
generatedHTML = converter.makeHtml(this.getValue());
|
||||
}
|
||||
|
||||
// Talk to Ember
|
||||
this.component.sendAction('openModal', 'copy-html', {generatedHTML: generatedHTML});
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
init: init
|
||||
};
|
53
app/utils/ed-image-manager.js
Normal file
53
app/utils/ed-image-manager.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
var imageMarkdownRegex = /^!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim;
|
||||
|
||||
// Process the markdown content and find all of the locations where there is an image markdown block
|
||||
function parse(stringToParse) {
|
||||
var m, images = [];
|
||||
while ((m = imageMarkdownRegex.exec(stringToParse)) !== null) {
|
||||
images.push(m);
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
// Loop through all dropzones in the preview and find which one was the target of the upload
|
||||
function getZoneIndex(element) {
|
||||
var zones = document.querySelectorAll('.js-entry-preview .js-drop-zone'),
|
||||
i;
|
||||
|
||||
for (i = 0; i < zones.length; i += 1) {
|
||||
if (zones.item(i) === element) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Figure out the start and end of the selection range for the src in the markdown, so we know what to replace
|
||||
function getSrcRange(content, element) {
|
||||
var images = parse(content),
|
||||
index = getZoneIndex(element),
|
||||
replacement = {};
|
||||
|
||||
if (index > -1) {
|
||||
// [1] matches the alt test, and 2 matches the url between the ()
|
||||
// if the () are missing entirely, which is valid, [2] will be undefined and we'll need to treat this case
|
||||
// a little differently
|
||||
if (images[index][2] === undefined) {
|
||||
replacement.needsParens = true;
|
||||
replacement.start = content.indexOf(']', images[index].index) + 1;
|
||||
replacement.end = replacement.start;
|
||||
} else {
|
||||
replacement.start = content.indexOf('(', images[index].index) + 1;
|
||||
replacement.end = replacement.start + images[index][2].length;
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default {
|
||||
getSrcRange: getSrcRange
|
||||
};
|
|
@ -1,3 +1,6 @@
|
|||
// # Editor shortcuts
|
||||
// Loaded by EditorBaseRoute, which is a shortcuts route
|
||||
// This map is used to ensure the right action is called by each shortcut
|
||||
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
|
||||
|
||||
var shortcuts = {};
|
||||
|
@ -6,27 +9,27 @@ var shortcuts = {};
|
|||
shortcuts[ctrlOrCmd + '+alt+p'] = 'publish';
|
||||
shortcuts['alt+shift+z'] = 'toggleZenMode';
|
||||
|
||||
// CodeMirror Markdown Shortcuts
|
||||
// Markdown Shortcuts
|
||||
|
||||
// Text
|
||||
shortcuts['ctrl+alt+u'] = {action: 'codeMirrorShortcut', options: {type: 'strike'}};
|
||||
shortcuts[ctrlOrCmd + '+b'] = {action: 'codeMirrorShortcut', options: {type: 'bold'}};
|
||||
shortcuts[ctrlOrCmd + '+i'] = {action: 'codeMirrorShortcut', options: {type: 'italic'}};
|
||||
shortcuts['ctrl+alt+u'] = {action: 'editorShortcut', options: {type: 'strike'}};
|
||||
shortcuts[ctrlOrCmd + '+b'] = {action: 'editorShortcut', options: {type: 'bold'}};
|
||||
shortcuts[ctrlOrCmd + '+i'] = {action: 'editorShortcut', options: {type: 'italic'}};
|
||||
|
||||
shortcuts['ctrl+u'] = {action: 'codeMirrorShortcut', options: {type: 'uppercase'}};
|
||||
shortcuts['ctrl+shift+u'] = {action: 'codeMirrorShortcut', options: {type: 'lowercase'}};
|
||||
shortcuts['ctrl+alt+shift+u'] = {action: 'codeMirrorShortcut', options: {type: 'titlecase'}};
|
||||
shortcuts[ctrlOrCmd + '+shift+c'] = {action: 'codeMirrorShortcut', options: {type: 'copyHTML'}};
|
||||
shortcuts[ctrlOrCmd + '+h'] = {action: 'codeMirrorShortcut', options: {type: 'cycleHeaderLevel'}};
|
||||
shortcuts['ctrl+u'] = {action: 'editorShortcut', options: {type: 'uppercase'}};
|
||||
shortcuts['ctrl+shift+u'] = {action: 'editorShortcut', options: {type: 'lowercase'}};
|
||||
shortcuts['ctrl+alt+shift+u'] = {action: 'editorShortcut', options: {type: 'titlecase'}};
|
||||
shortcuts[ctrlOrCmd + '+shift+c'] = {action: 'editorShortcut', options: {type: 'copyHTML'}};
|
||||
shortcuts[ctrlOrCmd + '+h'] = {action: 'editorShortcut', options: {type: 'cycleHeaderLevel'}};
|
||||
|
||||
// Formatting
|
||||
shortcuts['ctrl+q'] = {action: 'codeMirrorShortcut', options: {type: 'blockquote'}};
|
||||
shortcuts['ctrl+l'] = {action: 'codeMirrorShortcut', options: {type: 'list'}};
|
||||
shortcuts['ctrl+q'] = {action: 'editorShortcut', options: {type: 'blockquote'}};
|
||||
shortcuts['ctrl+l'] = {action: 'editorShortcut', options: {type: 'list'}};
|
||||
|
||||
// Insert content
|
||||
shortcuts['ctrl+shift+1'] = {action: 'codeMirrorShortcut', options: {type: 'currentDate'}};
|
||||
shortcuts[ctrlOrCmd + '+k'] = {action: 'codeMirrorShortcut', options: {type: 'link'}};
|
||||
shortcuts[ctrlOrCmd + '+shift+i'] = {action: 'codeMirrorShortcut', options: {type: 'image'}};
|
||||
shortcuts[ctrlOrCmd + '+shift+k'] = {action: 'codeMirrorShortcut', options: {type: 'code'}};
|
||||
shortcuts['ctrl+shift+1'] = {action: 'editorShortcut', options: {type: 'currentDate'}};
|
||||
shortcuts[ctrlOrCmd + '+k'] = {action: 'editorShortcut', options: {type: 'link'}};
|
||||
shortcuts[ctrlOrCmd + '+shift+i'] = {action: 'editorShortcut', options: {type: 'image'}};
|
||||
shortcuts[ctrlOrCmd + '+shift+k'] = {action: 'editorShortcut', options: {type: 'code'}};
|
||||
|
||||
export default shortcuts;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"name": "ghost",
|
||||
"dependencies": {
|
||||
"codemirror": "4.0.1",
|
||||
"Countable": "2.0.2",
|
||||
"device": "git://github.com/matthewhudson/device.js#5347a275b66020a0d4dfe9aad81a488f8cce448d",
|
||||
"ember": "1.10.0",
|
||||
|
@ -23,6 +22,7 @@
|
|||
"nanoscroller": "0.8.4",
|
||||
"normalize-scss": "~3.0.1",
|
||||
"nprogress": "0.1.2",
|
||||
"rangyinputs": "1.2.0",
|
||||
"showdown-ghost": "0.3.4",
|
||||
"validator-js": "3.28.0"
|
||||
},
|
||||
|
|
21
vendor/showdown/extensions/ghostimagepreview.js
vendored
21
vendor/showdown/extensions/ghostimagepreview.js
vendored
|
@ -1,5 +1,4 @@
|
|||
/* jshint node:true, browser:true */
|
||||
/* global Ember */
|
||||
|
||||
// Ghost Image Preview
|
||||
//
|
||||
|
@ -15,12 +14,12 @@ var Ghost = Ghost || {};
|
|||
{
|
||||
type: 'lang',
|
||||
filter: function (text) {
|
||||
var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
|
||||
/* regex from isURL in node-validator. Yum! */
|
||||
var imageMarkdownRegex = /^!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
|
||||
/* regex from isURL in node-validator. Yum! */
|
||||
uriRegex = /^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i,
|
||||
pathRegex = /^(\/)?([^\/\0]+(\/)?)+$/i;
|
||||
|
||||
return text.replace(imageMarkdownRegex, function (match, key, alt, src) {
|
||||
return text.replace(imageMarkdownRegex, function (match, alt, src) {
|
||||
var result = '',
|
||||
output;
|
||||
|
||||
|
@ -28,15 +27,11 @@ var Ghost = Ghost || {};
|
|||
result = '<img class="js-upload-target" src="' + src + '"/>';
|
||||
}
|
||||
|
||||
if ((Ghost && Ghost.touchEditor) || (typeof window !== 'undefined' && Ember.touchEditor)) {
|
||||
output = '<section class="image-uploader">' +
|
||||
result + '<div class="description">Mobile uploads coming soon</div></section>';
|
||||
} else {
|
||||
output = '<section id="image_upload_' + key + '" class="js-drop-zone image-uploader">' +
|
||||
result + '<div class="description">Add image of <strong>' + alt + '</strong></div>' +
|
||||
'<input data-url="upload" class="js-fileupload main fileupload" type="file" name="uploadimage">' +
|
||||
'</section>';
|
||||
}
|
||||
output = '<section class="js-drop-zone image-uploader">' +
|
||||
result +
|
||||
'<div class="description">Add image of <strong>' + alt + '</strong></div>' +
|
||||
'<input data-url="upload" class="js-fileupload main fileupload" type="file" name="uploadimage">' +
|
||||
'</section>';
|
||||
|
||||
return output;
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue