mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
Implement Mobile Editor
closes #2957 - add FastClick library to Gruntfile.js - add touch-editor to client/assets/lib/ - add mobile-specific utils to util/mobile-utils.js - add codemirror util to set up TouchEditor only if we're really on mobile - change gh-codemirror from having a default action to a named action. prevents Ember.TextArea firing action on change - change gh-codemirror `cm.getDoc().getValue()` to `cm.getValue()` for portability - change codemirror-shortcuts ES6 export/import style - changed ghostimagepreview.js to check for Ember.touchEditor in addition to Ghost.touchEditor
This commit is contained in:
parent
e0587ed79b
commit
6658675646
@ -512,6 +512,7 @@ var path = require('path'),
|
||||
|
||||
'bower_components/jquery-ui/ui/jquery-ui.js',
|
||||
'bower_components/jquery-file-upload/js/jquery.fileupload.js',
|
||||
'bower_components/fastclick/lib/fastclick.js',
|
||||
'bower_components/nprogress/nprogress.js',
|
||||
|
||||
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
|
||||
|
55
core/client/assets/lib/touch-editor.js
Normal file
55
core/client/assets/lib/touch-editor.js
Normal file
@ -0,0 +1,55 @@
|
||||
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
|
||||
};
|
||||
|
||||
return TouchEditor;
|
||||
};
|
||||
|
||||
export default createTouchEditor;
|
@ -1,8 +1,11 @@
|
||||
/*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 'ghost/utils/codemirror-shortcuts';
|
||||
import codeMirrorShortcuts from 'ghost/utils/codemirror-shortcuts';
|
||||
|
||||
codeMirrorShortcuts.init();
|
||||
|
||||
var onChangeHandler = function (cm, changeObj) {
|
||||
var line,
|
||||
@ -18,7 +21,7 @@ var onChangeHandler = function (cm, changeObj) {
|
||||
// Is this a line which may have had a marker on it?
|
||||
checkMarkers();
|
||||
|
||||
cm.component.set('value', cm.getDoc().getValue());
|
||||
cm.component.set('value', cm.getValue());
|
||||
};
|
||||
|
||||
var onScrollHandler = function (cm) {
|
||||
@ -41,9 +44,12 @@ var Codemirror = Ember.TextArea.extend(MarkerManager, {
|
||||
afterRenderEvent: function () {
|
||||
var initMarkers = _.bind(this.initMarkers, this);
|
||||
|
||||
// replaces CodeMirror with TouchEditor only if we're on mobile
|
||||
mobileCodeMirror.createIfMobile();
|
||||
|
||||
this.initCodemirror();
|
||||
this.codemirror.eachLine(initMarkers);
|
||||
this.sendAction('action', this);
|
||||
this.sendAction('setCodeMirror', this);
|
||||
},
|
||||
|
||||
// this needs to be placed on the 'afterRender' queue otherwise CodeMirror gets wonky
|
||||
|
@ -10,7 +10,7 @@
|
||||
<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=scratch scrollInfo=view.markdownScrollInfo action="setCodeMirror"}}
|
||||
{{gh-codemirror value=scratch scrollInfo=view.markdownScrollInfo setCodeMirror="setCodeMirror"}}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
45
core/client/utils/codemirror-mobile.js
Normal file
45
core/client/utils/codemirror-mobile.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*global CodeMirror*/
|
||||
import mobileUtils from 'ghost/utils/mobile-utils';
|
||||
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() {
|
||||
if (mobileUtils.hasTouchScreen()) {
|
||||
$('body').addClass('touch-editor');
|
||||
|
||||
// make editor tabs touch-to-toggle in portrait mode
|
||||
$('.floatingheader').on('touchstart', function () {
|
||||
$('.entry-markdown').toggleClass('active');
|
||||
$('.entry-preview').toggleClass('active');
|
||||
});
|
||||
|
||||
Ember.touchEditor = true;
|
||||
mobileUtils.initFastClick();
|
||||
TouchEditor = createTouchEditor();
|
||||
setupMobileCodeMirror();
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
createIfMobile: init
|
||||
};
|
@ -3,129 +3,135 @@
|
||||
* See editor-route-base
|
||||
*/
|
||||
|
||||
//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},
|
||||
md, letterCount, textIndex, position;
|
||||
switch (type) {
|
||||
case 'h1':
|
||||
this.replaceRange('# ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 2);
|
||||
return;
|
||||
case 'h2':
|
||||
this.replaceRange('## ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 3);
|
||||
return;
|
||||
case 'h3':
|
||||
this.replaceRange('### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 4);
|
||||
return;
|
||||
case 'h4':
|
||||
this.replaceRange('#### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 5);
|
||||
return;
|
||||
case 'h5':
|
||||
this.replaceRange('##### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 6);
|
||||
return;
|
||||
case 'h6':
|
||||
this.replaceRange('###### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 7);
|
||||
return;
|
||||
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;
|
||||
/** @TODO
|
||||
case 'uppercase':
|
||||
md = text.toLocaleUpperCase();
|
||||
break;
|
||||
case 'lowercase':
|
||||
md = text.toLocaleLowerCase();
|
||||
break;
|
||||
case 'titlecase':
|
||||
md = text.toTitleCase();
|
||||
break;
|
||||
case 'selectword':
|
||||
word = this.getTokenAt(cursor);
|
||||
if (!/\w$/g.test(word.string)) {
|
||||
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end - 1});
|
||||
} else {
|
||||
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end});
|
||||
}
|
||||
break;
|
||||
case 'copyHTML':
|
||||
converter = new Showdown.converter();
|
||||
if (text) {
|
||||
md = converter.makeHtml(text);
|
||||
} else {
|
||||
md = converter.makeHtml(this.getValue());
|
||||
}
|
||||
function init() {
|
||||
//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},
|
||||
md, letterCount, textIndex, position;
|
||||
switch (type) {
|
||||
case 'h1':
|
||||
this.replaceRange('# ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 2);
|
||||
return;
|
||||
case 'h2':
|
||||
this.replaceRange('## ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 3);
|
||||
return;
|
||||
case 'h3':
|
||||
this.replaceRange('### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 4);
|
||||
return;
|
||||
case 'h4':
|
||||
this.replaceRange('#### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 5);
|
||||
return;
|
||||
case 'h5':
|
||||
this.replaceRange('##### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 6);
|
||||
return;
|
||||
case 'h6':
|
||||
this.replaceRange('###### ' + line, fromLineStart);
|
||||
this.setCursor(cursor.line, cursor.ch + 7);
|
||||
return;
|
||||
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;
|
||||
/** @TODO
|
||||
case 'uppercase':
|
||||
md = text.toLocaleUpperCase();
|
||||
break;
|
||||
case 'lowercase':
|
||||
md = text.toLocaleLowerCase();
|
||||
break;
|
||||
case 'titlecase':
|
||||
md = text.toTitleCase();
|
||||
break;
|
||||
case 'selectword':
|
||||
word = this.getTokenAt(cursor);
|
||||
if (!/\w$/g.test(word.string)) {
|
||||
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end - 1});
|
||||
} else {
|
||||
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end});
|
||||
}
|
||||
break;
|
||||
case 'copyHTML':
|
||||
converter = new Showdown.converter();
|
||||
if (text) {
|
||||
md = converter.makeHtml(text);
|
||||
} else {
|
||||
md = converter.makeHtml(this.getValue());
|
||||
}
|
||||
|
||||
$(".modal-copyToHTML-content").text(md).selectText();
|
||||
break;
|
||||
case 'newLine':
|
||||
if (line !== "") {
|
||||
this.replaceRange(line + "\n\n", fromLineStart);
|
||||
$(".modal-copyToHTML-content").text(md).selectText();
|
||||
break;
|
||||
case 'newLine':
|
||||
if (line !== "") {
|
||||
this.replaceRange(line + "\n\n", fromLineStart);
|
||||
}
|
||||
break;
|
||||
*/
|
||||
default:
|
||||
if (this.simpleShortcutSyntax[type]) {
|
||||
md = this.simpleShortcutSyntax[type].replace('$1', text);
|
||||
}
|
||||
}
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
48
core/client/utils/mobile-utils.js
Normal file
48
core/client/utils/mobile-utils.js
Normal file
@ -0,0 +1,48 @@
|
||||
/*global DocumentTouch,FastClick*/
|
||||
var hasTouchScreen,
|
||||
smallScreen,
|
||||
initFastClick,
|
||||
responsiveAction;
|
||||
|
||||
// Taken from "Responsive design & the Guardian" with thanks to Matt Andrews
|
||||
// Added !window._phantom so that the functional tests run as though this is not a touch screen.
|
||||
// In future we can do something more advanced here for testing both touch and non touch
|
||||
hasTouchScreen = function () {
|
||||
return !window._phantom &&
|
||||
(
|
||||
('ontouchstart' in window) ||
|
||||
(window.DocumentTouch && document instanceof DocumentTouch)
|
||||
);
|
||||
};
|
||||
|
||||
smallScreen = function () {
|
||||
if (window.matchMedia('(max-width: 1000px)').matches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
initFastClick = function () {
|
||||
Ember.run.scheduleOnce('afterRender', null, function () {
|
||||
FastClick.attach(document.body);
|
||||
});
|
||||
};
|
||||
|
||||
responsiveAction = function responsiveAction(event, mediaCondition, cb) {
|
||||
if (!window.matchMedia(mediaCondition).matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cb();
|
||||
};
|
||||
|
||||
export { hasTouchScreen, smallScreen };
|
||||
export default {
|
||||
hasTouchScreen: hasTouchScreen,
|
||||
smallScreen: smallScreen,
|
||||
initFastClick: initFastClick,
|
||||
responsiveAction: responsiveAction
|
||||
};
|
@ -28,7 +28,7 @@ var Ghost = Ghost || {};
|
||||
result = '<img class="js-upload-target" src="' + src + '"/>';
|
||||
}
|
||||
|
||||
if (Ghost && Ghost.touchEditor) {
|
||||
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 {
|
||||
|
Loading…
Reference in New Issue
Block a user