mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05: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
8 changed files with 288 additions and 127 deletions
|
@ -512,6 +512,7 @@ var path = require('path'),
|
||||||
|
|
||||||
'bower_components/jquery-ui/ui/jquery-ui.js',
|
'bower_components/jquery-ui/ui/jquery-ui.js',
|
||||||
'bower_components/jquery-file-upload/js/jquery.fileupload.js',
|
'bower_components/jquery-file-upload/js/jquery.fileupload.js',
|
||||||
|
'bower_components/fastclick/lib/fastclick.js',
|
||||||
'bower_components/nprogress/nprogress.js',
|
'bower_components/nprogress/nprogress.js',
|
||||||
|
|
||||||
'core/shared/lib/showdown/extensions/ghostimagepreview.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 */
|
/*global CodeMirror */
|
||||||
|
|
||||||
import MarkerManager from 'ghost/mixins/marker-manager';
|
import MarkerManager from 'ghost/mixins/marker-manager';
|
||||||
|
import mobileCodeMirror from 'ghost/utils/codemirror-mobile';
|
||||||
import setScrollClassName from 'ghost/utils/set-scroll-classname';
|
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 onChangeHandler = function (cm, changeObj) {
|
||||||
var line,
|
var line,
|
||||||
|
@ -18,7 +21,7 @@ var onChangeHandler = function (cm, changeObj) {
|
||||||
// Is this a line which may have had a marker on it?
|
// Is this a line which may have had a marker on it?
|
||||||
checkMarkers();
|
checkMarkers();
|
||||||
|
|
||||||
cm.component.set('value', cm.getDoc().getValue());
|
cm.component.set('value', cm.getValue());
|
||||||
};
|
};
|
||||||
|
|
||||||
var onScrollHandler = function (cm) {
|
var onScrollHandler = function (cm) {
|
||||||
|
@ -41,9 +44,12 @@ var Codemirror = Ember.TextArea.extend(MarkerManager, {
|
||||||
afterRenderEvent: function () {
|
afterRenderEvent: function () {
|
||||||
var initMarkers = _.bind(this.initMarkers, this);
|
var initMarkers = _.bind(this.initMarkers, this);
|
||||||
|
|
||||||
|
// replaces CodeMirror with TouchEditor only if we're on mobile
|
||||||
|
mobileCodeMirror.createIfMobile();
|
||||||
|
|
||||||
this.initCodemirror();
|
this.initCodemirror();
|
||||||
this.codemirror.eachLine(initMarkers);
|
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
|
// 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>
|
<a class="markdown-help" href="" {{action "openModal" "markdown"}}><span class="hidden">What is Markdown?</span></a>
|
||||||
</header>
|
</header>
|
||||||
<section id="entry-markdown-content" class="entry-markdown-content">
|
<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>
|
||||||
</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
|
* See editor-route-base
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//Used for simple, noncomputational replace-and-go! shortcuts.
|
function init() {
|
||||||
// See default case in shortcut function below.
|
//Used for simple, noncomputational replace-and-go! shortcuts.
|
||||||
CodeMirror.prototype.simpleShortcutSyntax = {
|
// See default case in shortcut function below.
|
||||||
bold: '**$1**',
|
CodeMirror.prototype.simpleShortcutSyntax = {
|
||||||
italic: '*$1*',
|
bold: '**$1**',
|
||||||
strike: '~~$1~~',
|
italic: '*$1*',
|
||||||
code: '`$1`',
|
strike: '~~$1~~',
|
||||||
link: '[$1](http://)',
|
code: '`$1`',
|
||||||
image: '![$1](http://)',
|
link: '[$1](http://)',
|
||||||
blockquote: '> $1'
|
image: '![$1](http://)',
|
||||||
};
|
blockquote: '> $1'
|
||||||
CodeMirror.prototype.shortcut = function (type) {
|
};
|
||||||
var text = this.getSelection(),
|
CodeMirror.prototype.shortcut = function (type) {
|
||||||
cursor = this.getCursor(),
|
var text = this.getSelection(),
|
||||||
line = this.getLine(cursor.line),
|
cursor = this.getCursor(),
|
||||||
fromLineStart = {line: cursor.line, ch: 0},
|
line = this.getLine(cursor.line),
|
||||||
md, letterCount, textIndex, position;
|
fromLineStart = {line: cursor.line, ch: 0},
|
||||||
switch (type) {
|
md, letterCount, textIndex, position;
|
||||||
case 'h1':
|
switch (type) {
|
||||||
this.replaceRange('# ' + line, fromLineStart);
|
case 'h1':
|
||||||
this.setCursor(cursor.line, cursor.ch + 2);
|
this.replaceRange('# ' + line, fromLineStart);
|
||||||
return;
|
this.setCursor(cursor.line, cursor.ch + 2);
|
||||||
case 'h2':
|
return;
|
||||||
this.replaceRange('## ' + line, fromLineStart);
|
case 'h2':
|
||||||
this.setCursor(cursor.line, cursor.ch + 3);
|
this.replaceRange('## ' + line, fromLineStart);
|
||||||
return;
|
this.setCursor(cursor.line, cursor.ch + 3);
|
||||||
case 'h3':
|
return;
|
||||||
this.replaceRange('### ' + line, fromLineStart);
|
case 'h3':
|
||||||
this.setCursor(cursor.line, cursor.ch + 4);
|
this.replaceRange('### ' + line, fromLineStart);
|
||||||
return;
|
this.setCursor(cursor.line, cursor.ch + 4);
|
||||||
case 'h4':
|
return;
|
||||||
this.replaceRange('#### ' + line, fromLineStart);
|
case 'h4':
|
||||||
this.setCursor(cursor.line, cursor.ch + 5);
|
this.replaceRange('#### ' + line, fromLineStart);
|
||||||
return;
|
this.setCursor(cursor.line, cursor.ch + 5);
|
||||||
case 'h5':
|
return;
|
||||||
this.replaceRange('##### ' + line, fromLineStart);
|
case 'h5':
|
||||||
this.setCursor(cursor.line, cursor.ch + 6);
|
this.replaceRange('##### ' + line, fromLineStart);
|
||||||
return;
|
this.setCursor(cursor.line, cursor.ch + 6);
|
||||||
case 'h6':
|
return;
|
||||||
this.replaceRange('###### ' + line, fromLineStart);
|
case 'h6':
|
||||||
this.setCursor(cursor.line, cursor.ch + 7);
|
this.replaceRange('###### ' + line, fromLineStart);
|
||||||
return;
|
this.setCursor(cursor.line, cursor.ch + 7);
|
||||||
case 'link':
|
return;
|
||||||
md = this.simpleShortcutSyntax.link.replace('$1', text);
|
case 'link':
|
||||||
this.replaceSelection(md, 'end');
|
md = this.simpleShortcutSyntax.link.replace('$1', text);
|
||||||
if (!text) {
|
this.replaceSelection(md, 'end');
|
||||||
this.setCursor(cursor.line, cursor.ch + 1);
|
if (!text) {
|
||||||
} else {
|
this.setCursor(cursor.line, cursor.ch + 1);
|
||||||
textIndex = line.indexOf(text, cursor.ch - text.length);
|
} else {
|
||||||
position = textIndex + md.length - 1;
|
textIndex = line.indexOf(text, cursor.ch - text.length);
|
||||||
this.setSelection({
|
position = textIndex + md.length - 1;
|
||||||
line: cursor.line,
|
this.setSelection({
|
||||||
ch: position - 7
|
line: cursor.line,
|
||||||
}, {
|
ch: position - 7
|
||||||
line: cursor.line,
|
}, {
|
||||||
ch: position
|
line: cursor.line,
|
||||||
});
|
ch: position
|
||||||
}
|
});
|
||||||
return;
|
}
|
||||||
case 'image':
|
return;
|
||||||
md = this.simpleShortcutSyntax.image.replace('$1', text);
|
case 'image':
|
||||||
if (line !== '') {
|
md = this.simpleShortcutSyntax.image.replace('$1', text);
|
||||||
md = '\n\n' + md;
|
if (line !== '') {
|
||||||
}
|
md = '\n\n' + md;
|
||||||
this.replaceSelection(md, 'end');
|
}
|
||||||
cursor = this.getCursor();
|
this.replaceSelection(md, 'end');
|
||||||
this.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1});
|
cursor = this.getCursor();
|
||||||
return;
|
this.setSelection({line: cursor.line, ch: cursor.ch - 8}, {line: cursor.line, ch: cursor.ch - 1});
|
||||||
case 'list':
|
return;
|
||||||
md = text.replace(/^(\s*)(\w\W*)/gm, '$1* $2');
|
case 'list':
|
||||||
this.replaceSelection(md, 'end');
|
md = text.replace(/^(\s*)(\w\W*)/gm, '$1* $2');
|
||||||
return;
|
this.replaceSelection(md, 'end');
|
||||||
case 'currentDate':
|
return;
|
||||||
md = moment(new Date()).format('D MMMM YYYY');
|
case 'currentDate':
|
||||||
this.replaceSelection(md, 'end');
|
md = moment(new Date()).format('D MMMM YYYY');
|
||||||
return;
|
this.replaceSelection(md, 'end');
|
||||||
/** @TODO
|
return;
|
||||||
case 'uppercase':
|
/** @TODO
|
||||||
md = text.toLocaleUpperCase();
|
case 'uppercase':
|
||||||
break;
|
md = text.toLocaleUpperCase();
|
||||||
case 'lowercase':
|
break;
|
||||||
md = text.toLocaleLowerCase();
|
case 'lowercase':
|
||||||
break;
|
md = text.toLocaleLowerCase();
|
||||||
case 'titlecase':
|
break;
|
||||||
md = text.toTitleCase();
|
case 'titlecase':
|
||||||
break;
|
md = text.toTitleCase();
|
||||||
case 'selectword':
|
break;
|
||||||
word = this.getTokenAt(cursor);
|
case 'selectword':
|
||||||
if (!/\w$/g.test(word.string)) {
|
word = this.getTokenAt(cursor);
|
||||||
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end - 1});
|
if (!/\w$/g.test(word.string)) {
|
||||||
} else {
|
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end - 1});
|
||||||
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end});
|
} else {
|
||||||
}
|
this.setSelection({line: cursor.line, ch: word.start}, {line: cursor.line, ch: word.end});
|
||||||
break;
|
}
|
||||||
case 'copyHTML':
|
break;
|
||||||
converter = new Showdown.converter();
|
case 'copyHTML':
|
||||||
if (text) {
|
converter = new Showdown.converter();
|
||||||
md = converter.makeHtml(text);
|
if (text) {
|
||||||
} else {
|
md = converter.makeHtml(text);
|
||||||
md = converter.makeHtml(this.getValue());
|
} else {
|
||||||
}
|
md = converter.makeHtml(this.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
$(".modal-copyToHTML-content").text(md).selectText();
|
$(".modal-copyToHTML-content").text(md).selectText();
|
||||||
break;
|
break;
|
||||||
case 'newLine':
|
case 'newLine':
|
||||||
if (line !== "") {
|
if (line !== "") {
|
||||||
this.replaceRange(line + "\n\n", fromLineStart);
|
this.replaceRange(line + "\n\n", fromLineStart);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
*/
|
||||||
|
default:
|
||||||
|
if (this.simpleShortcutSyntax[type]) {
|
||||||
|
md = this.simpleShortcutSyntax[type].replace('$1', text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
if (md) {
|
||||||
*/
|
this.replaceSelection(md, 'end');
|
||||||
default:
|
if (!text) {
|
||||||
if (this.simpleShortcutSyntax[type]) {
|
letterCount = md.length;
|
||||||
md = this.simpleShortcutSyntax[type].replace('$1', text);
|
this.setCursor({
|
||||||
|
line: cursor.line,
|
||||||
|
ch: cursor.ch + (letterCount / 2)
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
if (md) {
|
}
|
||||||
this.replaceSelection(md, 'end');
|
|
||||||
if (!text) {
|
export default {
|
||||||
letterCount = md.length;
|
init: init
|
||||||
this.setCursor({
|
|
||||||
line: cursor.line,
|
|
||||||
ch: cursor.ch + (letterCount / 2)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
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 + '"/>';
|
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">' +
|
output = '<section class="image-uploader">' +
|
||||||
result + '<div class="description">Mobile uploads coming soon</div></section>';
|
result + '<div class="description">Mobile uploads coming soon</div></section>';
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Add table
Reference in a new issue