Fix split screen editor (#684)

no issue

* fix title input padding and placeholder weight
* 🔥 remove unused showdown-ghost dependency
* implement full screen mode via CSS rather than autonav toggle
* implement custom split pane editor preview
    - replace SimpleMDE's split pane handling with our own so that we have more control over the element positioning, toggling of our custom fullscreen code, and so that the preview pane can be scrolled separately as per our old editor
* use forked version of simplemde that has the latest CodeMirror compiled
    - SimpleMDE hasn't been updated for 11 months and the version of CodeMirror is baked into the SimpleMDE code, to get an up to date version I've forked and re-compiled
    - pull in the unminified SimpleMDE source so that it's easier to debug in development, our asset compilation steps will take care of minifying it for production
* fix gh-markdown-editor teardown
This commit is contained in:
Kevin Ansfield 2017-05-08 19:15:56 +01:00 committed by Hannah Wolfe
parent 762c3c4df0
commit fb2fa06b48
10 changed files with 279 additions and 60 deletions

View File

@ -9,7 +9,10 @@ const {debounce} = run;
export default Component.extend({
classNameBindings: ['isDraggedOver:-drag-over'],
classNameBindings: [
'isDraggedOver:-drag-over',
'isFullScreen:gh-editor-fullscreen'
],
// Public attributes
navIsClosed: false,
@ -20,14 +23,12 @@ export default Component.extend({
imageExtensions: IMAGE_EXTENSIONS,
imageMimeTypes: IMAGE_MIME_TYPES,
isDraggedOver: false,
isFullScreen: false,
isSplitScreen: false,
uploadedImageUrls: null,
// Closure actions
toggleAutoNav() {},
// Private
_dragCounter: 0,
_fullScreenEnabled: false,
_navIsClosed: false,
_onResizeHandler: null,
_viewActionsWidth: 190,
@ -49,7 +50,6 @@ export default Component.extend({
let navIsClosed = this.get('navIsClosed');
if (navIsClosed !== this._navIsClosed) {
this._fullScreenEnabled = navIsClosed;
run.scheduleOnce('afterRender', this, this._setHeaderClass);
}
@ -58,13 +58,19 @@ export default Component.extend({
_setHeaderClass() {
let $editorTitle = this.$('.gh-editor-title');
let smallHeaderClass = 'gh-editor-header-small';
if (this.get('isSplitScreen')) {
this.set('headerClass', smallHeaderClass);
return;
}
if ($editorTitle.length > 0) {
let boundingRect = $editorTitle[0].getBoundingClientRect();
let maxRight = window.innerWidth - this._viewActionsWidth;
if (boundingRect.right >= maxRight) {
this.set('headerClass', 'gh-editor-header-small');
this.set('headerClass', smallHeaderClass);
return;
}
}
@ -128,22 +134,17 @@ export default Component.extend({
willDestroyElement() {
this._super(...arguments);
window.removeEventListener('resize', this._onResizeHandler);
// reset fullscreen mode if it was turned on
if (this._fullScreenEnabled) {
this.toggleAutoNav();
}
},
actions: {
toggleFullScreen() {
if (!this._fullScreenWasToggled) {
this._fullScreenEnabled = !this.get('isNavOpen');
this._fullScreenWasToggled = true;
} else {
this._fullScreenEnabled = !this._fullScreenEnabled;
}
this.toggleAutoNav();
this.toggleProperty('isFullScreen');
run.scheduleOnce('afterRender', this, this._setHeaderClass);
},
toggleSplitScreen() {
this.toggleProperty('isSplitScreen');
run.scheduleOnce('afterRender', this, this._setHeaderClass);
},
uploadComplete(uploads) {

View File

@ -22,8 +22,15 @@ export const BLANK_DOC = {
export default Component.extend({
classNames: ['gh-markdown-editor'],
classNameBindings: [
'_isFullScreen:gh-markdown-editor-full-screen',
'_isSplitScreen:gh-markdown-editor-side-by-side'
],
// Public attributes
autofocus: false,
isFullScreen: false,
mobiledoc: null,
options: null,
placeholder: '',
@ -32,6 +39,7 @@ export default Component.extend({
// Closure actions
onChange() {},
onFullScreen() {},
onSplitScreen() {},
showMarkdownHelp() {},
// Internal attributes
@ -39,10 +47,12 @@ export default Component.extend({
// Private
_editor: null,
_isFullScreen: false,
_isSplitScreen: false,
_isUploading: false,
_uploadedImageUrls: null,
_statusbar: null,
_toolbar: null,
_uploadedImageUrls: null,
// Ghost-Specific SimpleMDE toolbar config - allows us to create a bridge
// between SimpleMDE buttons and Ember actions
@ -53,14 +63,22 @@ export default Component.extend({
'bold', 'italic', 'heading', '|',
'quote', 'unordered-list', 'ordered-list', '|',
'link', 'image', '|',
'preview', 'side-by-side',
'preview',
{
name: 'side-by-side',
action: () => {
this.send('toggleSplitScreen');
},
className: 'fa fa-columns no-disable no-mobile',
title: 'Toggle Side by Side'
},
{
name: 'fullscreen',
action: () => {
this.onFullScreen();
this.send('toggleFullScreen');
},
className: 'fa fa-arrows-alt no-disable no-mobile',
title: 'Toggle Fullscreen (F11)'
title: 'Toggle Fullscreen'
},
'|',
{
@ -97,6 +115,16 @@ export default Component.extend({
// eslint-disable-next-line ember-suave/prefer-destructuring
let markdown = mobiledoc.cards[0][1].markdown;
this.set('markdown', markdown);
// use internal values to avoid updating bound values
if (!isEmpty(this.get('isFullScreen'))) {
this.set('_isFullScreen', this.get('isFullScreen'));
}
if (!isEmpty(this.get('isSplitScreen'))) {
this.set('_isSplitScreen', this.get('isSplitScreen'));
}
this._updateButtonState();
},
_insertImages(urls) {
@ -118,6 +146,100 @@ export default Component.extend({
cm.replaceSelection(text, 'end');
},
// mark the split-pane/full-screen buttons active when they're active
_updateButtonState() {
if (this._editor) {
let fullScreenButton = this._editor.toolbarElements.fullscreen;
let sideBySideButton = this._editor.toolbarElements['side-by-side'];
if (this.get('_isFullScreen')) {
fullScreenButton.classList.add('active');
} else {
fullScreenButton.classList.remove('active');
}
if (this.get('_isSplitScreen')) {
sideBySideButton.classList.add('active');
} else {
sideBySideButton.classList.remove('active');
}
}
},
// set up the preview auto-update and scroll sync
_connectSplitPreview() {
let cm = this._editor.codemirror;
let editor = this._editor;
/* eslint-disable ember-suave/prefer-destructuring */
let editorPane = this.$('.gh-markdown-editor-pane')[0];
let previewPane = this.$('.gh-markdown-editor-preview')[0];
let previewContent = this.$('.gh-markdown-editor-preview-content')[0];
/* eslint-enable ember-suave/prefer-destructuring */
this._editorPane = editorPane;
this._previewPane = previewPane;
this._previewContent = previewContent;
// from SimpleMDE -------
let sideBySideRenderingFunction = function() {
previewContent.innerHTML = editor.options.previewRender(
editor.value(),
previewContent
);
};
cm.sideBySideRenderingFunction = sideBySideRenderingFunction;
sideBySideRenderingFunction();
cm.on('update', cm.sideBySideRenderingFunction);
// Refresh to fix selection being off (#309)
cm.refresh();
// ----------------------
this._onEditorPaneScroll = this._scrollHandler.bind(this);
editorPane.addEventListener('scroll', this._onEditorPaneScroll, false);
this._scrollSync();
},
_scrollHandler() {
if (!this._scrollSyncTicking) {
requestAnimationFrame(this._scrollSync.bind(this));
}
this._scrollSyncTicking = true;
},
_scrollSync() {
let editorPane = this._editorPane;
let previewPane = this._previewPane;
let height = editorPane.scrollHeight - editorPane.clientHeight;
let ratio = parseFloat(editorPane.scrollTop) / height;
let move = (previewPane.scrollHeight - previewPane.clientHeight) * ratio;
previewPane.scrollTop = move;
this._scrollSyncTicking = false;
},
_disconnectSplitPreview() {
let cm = this._editor.codemirror;
cm.off('update', cm.sideBySideRenderingFunction);
cm.refresh();
this._editorPane.removeEventListener('scroll', this._onEditorPaneScroll, false);
delete this._previewPane;
delete this._previewPaneContent;
delete this._onEditorPaneScroll;
},
willDestroyElement() {
if (this.get('_isSplitScreen')) {
this._disconnectSplitPreview();
}
this._super(...arguments);
},
actions: {
// put the markdown into a new mobiledoc card, trigger external update
updateMarkdown(markdown) {
@ -142,15 +264,8 @@ export default Component.extend({
this._statusbar = this.$('.editor-statusbar');
this._toolbar.appendTo(container);
this._statusbar.appendTo(container);
},
// put the toolbar/statusbar elements back so that SimpleMDE doesn't throw
// errors when it tries to remove them
destroyEditor() {
let container = this.$();
this._toolbar.appendTo(container);
this._statusbar.appendTo(container);
this._editor = null;
this._updateButtonState();
},
// used by the title input when the TAB or ENTER keys are pressed
@ -164,6 +279,51 @@ export default Component.extend({
}
return false;
},
toggleFullScreen() {
let isFullScreen = !this.get('_isFullScreen');
this.set('_isFullScreen', isFullScreen);
this._updateButtonState();
this.onFullScreen(isFullScreen);
// leave split screen when exiting full screen mode
if (!isFullScreen && this.get('_isSplitScreen')) {
this.send('toggleSplitScreen');
}
},
toggleSplitScreen() {
let isSplitScreen = !this.get('_isSplitScreen');
this.set('_isSplitScreen', isSplitScreen);
this._updateButtonState();
// set up the preview rendering and scroll sync
// afterRender is needed so that necessary components have been
// added/removed and editor pane length has settled
if (isSplitScreen) {
run.scheduleOnce('afterRender', this, this._connectSplitPreview);
} else {
run.scheduleOnce('afterRender', this, this._disconnectSplitPreview);
}
this.onSplitScreen(isSplitScreen);
// go fullscreen when entering split screen mode
if (isSplitScreen && !this.get('_isFullScreen')) {
this.send('toggleFullScreen');
}
},
// put the toolbar/statusbar elements back so that SimpleMDE doesn't throw
// errors when it tries to remove them
destroyEditor() {
let container = this.$('.gh-markdown-editor-pane');
this._toolbar.appendTo(container);
this._statusbar.appendTo(container);
this._editor = null;
}
}
});

View File

@ -141,6 +141,16 @@
/* NEW editor
/* ---------------------------------------------------------- */
.gh-main > section.gh-editor-fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background-color: white;
}
.gh-editor-header {
position: absolute;
top: 0;
@ -220,8 +230,42 @@
/* SimpleMDE editor
/* ---------------------------------------------------------- */
.gh-editor-container {
.gh-editor-title {
padding: 0;
}
.gh-editor-title:placeholder {
font-weight: bold;
}
.gh-markdown-editor {
position: relative;
width: 100%;
height: 100%;
overflow-y: auto;
z-index: 0;
}
.gh-markdown-editor-pane,
.gh-markdown-editor-preview {
padding: 10vw 4vw;
}
.gh-markdown-editor-side-by-side {
display: flex;
flex-direction: row;
overflow-y: hidden;
}
.gh-markdown-editor-side-by-side .gh-markdown-editor-pane,
.gh-markdown-editor-side-by-side .gh-markdown-editor-preview {
width: 50%;
height: 100%;
overflow-y: auto;
}
.gh-markdown-editor-preview {
border-left: 1px solid color(var(--lightgrey) l(+4%));
}
.gh-editor-footer {
@ -230,7 +274,7 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--lightgrey);
border-top: 1px solid color(var(--lightgrey) l(+4%));
}
.gh-editor-footer .editor-toolbar {

View File

@ -1,11 +1,13 @@
{{yield (hash
headerClass=headerClass
isDraggedOver=isDraggedOver
isFullScreen=isFullScreen
droppedFiles=droppedFiles
uploadedImageUrls=uploadedImageUrls
imageMimeTypes=imageMimeTypes
imageExtensions=imageExtensions
toggleFullScreen=(action "toggleFullScreen")
toggleSplitScreen=(action "toggleSplitScreen")
uploadComplete=(action "uploadComplete")
uploadCancelled=(action "uploadCancelled")
)}}

View File

@ -1,5 +1,5 @@
{{yield (hash
pane=(component "gh-simplemde"
editor=(component "gh-simplemde"
value=markdown
placeholder=placeholder
autofocus=autofocus
@ -7,5 +7,7 @@
onEditorInit=(action "setEditor")
onEditorDestroy=(action "destroyEditor")
options=simpleMDEOptions)
isFullScreen=_isFullScreen
isSplitScreen=_isSplitScreen
focus=(action "focusEditor")
)}}

View File

@ -2,7 +2,6 @@
tagName="section"
class="gh-editor gh-view"
navIsClosed=navIsClosed
toggleAutoNav=(action "toggleAutoNav")
as |editor|
}}
<header class="gh-editor-header {{editor.headerClass}}">
@ -35,33 +34,42 @@
access to the markdown editor's "focus" action
--}}
{{#gh-markdown-editor
class="gh-editor-container"
tabindex="2"
placeholder="Click here to start..."
autofocus=shouldFocusEditor
uploadedImageUrls=editor.uploadedImageUrls
mobiledoc=(readonly model.scratch)
isFullScreen=editor.isFullScreen
onChange=(action "updateScratch")
onFullScreen=(action editor.toggleFullScreen)
onSplitScreen=(action editor.toggleSplitScreen)
showMarkdownHelp=(route-action "toggleMarkdownHelpModal")
as |markdown|
}}
{{gh-trim-focus-input model.titleScratch
type="text"
class="gh-editor-title"
placeholder="Your Post Title"
tabindex="1"
shouldFocus=shouldFocusTitle
focus-out="updateTitle"
update=(action (perform updateTitle))
keyEvents=(hash
9=(action markdown.focus 'bottom')
13=(action markdown.focus 'top')
)
data-test-editor-title-input=true
}}
<div class="gh-markdown-editor-pane">
{{gh-trim-focus-input model.titleScratch
type="text"
class="gh-editor-title"
placeholder="Your Post Title"
tabindex="1"
shouldFocus=shouldFocusTitle
focus-out="updateTitle"
update=(action (perform updateTitle))
keyEvents=(hash
9=(action markdown.focus 'bottom')
13=(action markdown.focus 'top')
)
data-test-editor-title-input=true
}}
{{markdown.editor}}
</div>
{{markdown.pane}}
{{#if markdown.isSplitScreen}}
<div class="gh-markdown-editor-preview">
<h1>{{model.titleScratch}}</h1>
<div class="gh-markdown-editor-preview-content"></div>
</div>
{{/if}}
{{/gh-markdown-editor}}
{{!-- TODO: put tool/status bar in here so that scroll area can be fixed --}}

View File

@ -13,7 +13,6 @@
"pretender": "1.1.0",
"rangyinputs": "1.2.0",
"selectize": "~0.12.1",
"showdown-ghost": "0.3.6",
"validator-js": "3.39.0"
}
}

View File

@ -136,8 +136,8 @@ module.exports = function (defaults) {
import: ['lib/password-generator.js']
},
'simplemde': {
srcDir: 'dist',
import: ['simplemde.min.js', 'simplemde.min.css']
srcDir: 'debug',
import: ['simplemde.js', 'simplemde.css']
}
},
'ember-cli-selectize': {
@ -166,11 +166,6 @@ module.exports = function (defaults) {
// 'dem Scripts
app.import('bower_components/validator-js/validator.js');
app.import('bower_components/rangyinputs/rangyinputs-jquery-src.js');
app.import('bower_components/showdown-ghost/src/showdown.js');
app.import('bower_components/showdown-ghost/src/extensions/ghostgfm.js');
app.import('bower_components/showdown-ghost/src/extensions/ghostimagepreview.js');
app.import('bower_components/showdown-ghost/src/extensions/footnotes.js');
app.import('bower_components/showdown-ghost/src/extensions/highlight.js');
app.import('bower_components/keymaster/keymaster.js');
app.import('bower_components/devicejs/lib/device.js');

View File

@ -102,7 +102,7 @@
"postcss-color-function": "3.0.0",
"postcss-custom-properties": "5.0.2",
"postcss-easy-import": "2.0.0",
"simplemde": "1.11.2",
"simplemde": "https://github.com/kevinansfield/simplemde-markdown-editor.git",
"top-gh-contribs": "2.0.4",
"torii": "0.8.2",
"walk-sync": "0.3.1"

View File

@ -6592,6 +6592,14 @@ simplemde@1.11.2:
codemirror-spell-checker "*"
marked "*"
"simplemde@https://github.com/kevinansfield/simplemde-markdown-editor.git":
version "1.11.2"
resolved "https://github.com/kevinansfield/simplemde-markdown-editor.git#6abda7ab68cc20f4aca870eb243747951b90ab04"
dependencies:
codemirror "*"
codemirror-spell-checker "*"
marked "*"
sinon@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.1.0.tgz#e057a9d2bf1b32f5d6dd62628ca9ee3961b0cafb"