1
0
Fork 0
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:
Hannah Wolfe 2015-03-13 09:39:28 +00:00
parent 3c927411fa
commit 48996c767b
24 changed files with 687 additions and 1049 deletions

View file

@ -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');

View file

@ -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;

View file

@ -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;

View 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;

View 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;

View file

@ -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
View 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;

View 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;

View 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;

View file

@ -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) {

View file

@ -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);
}
},

View file

@ -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;
})
});

View file

@ -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;

View file

@ -14,7 +14,6 @@
@import "../../../../bower_components/normalize-scss/_normalize"; // via Bower
@import "vendor/nprogress";
@import "vendor/codemirror";
@import "vendor/nanoscroller";

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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
};

View file

@ -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
};

View 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
};

View file

@ -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;

View file

@ -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"
},

View file

@ -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;
});