mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
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:
parent
762c3c4df0
commit
fb2fa06b48
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
)}}
|
||||
|
@ -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")
|
||||
)}}
|
||||
|
@ -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 --}}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user