From fb2fa06b48cab99abca472e55826ed332b478e5e Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Mon, 8 May 2017 19:15:56 +0100 Subject: [PATCH] Fix split screen editor (#684) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ghost/admin/app/components/gh-editor.js | 39 ++-- .../app/components/gh-markdown-editor.js | 184 ++++++++++++++++-- ghost/admin/app/styles/layouts/editor.css | 48 ++++- .../app/templates/components/gh-editor.hbs | 2 + .../components/gh-markdown-editor.hbs | 4 +- ghost/admin/app/templates/editor/edit.hbs | 42 ++-- ghost/admin/bower.json | 1 - ghost/admin/ember-cli-build.js | 9 +- ghost/admin/package.json | 2 +- ghost/admin/yarn.lock | 8 + 10 files changed, 279 insertions(+), 60 deletions(-) diff --git a/ghost/admin/app/components/gh-editor.js b/ghost/admin/app/components/gh-editor.js index 2679e42b2d..b6b99cf744 100644 --- a/ghost/admin/app/components/gh-editor.js +++ b/ghost/admin/app/components/gh-editor.js @@ -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) { diff --git a/ghost/admin/app/components/gh-markdown-editor.js b/ghost/admin/app/components/gh-markdown-editor.js index edae5bc353..0770dbe53c 100644 --- a/ghost/admin/app/components/gh-markdown-editor.js +++ b/ghost/admin/app/components/gh-markdown-editor.js @@ -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; } } }); diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css index fc9fa03328..412aa0b802 100644 --- a/ghost/admin/app/styles/layouts/editor.css +++ b/ghost/admin/app/styles/layouts/editor.css @@ -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 { diff --git a/ghost/admin/app/templates/components/gh-editor.hbs b/ghost/admin/app/templates/components/gh-editor.hbs index dbcdeab043..c7f745a8ae 100644 --- a/ghost/admin/app/templates/components/gh-editor.hbs +++ b/ghost/admin/app/templates/components/gh-editor.hbs @@ -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") )}} diff --git a/ghost/admin/app/templates/components/gh-markdown-editor.hbs b/ghost/admin/app/templates/components/gh-markdown-editor.hbs index 0f5738a3a3..563cf4a957 100644 --- a/ghost/admin/app/templates/components/gh-markdown-editor.hbs +++ b/ghost/admin/app/templates/components/gh-markdown-editor.hbs @@ -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") )}} diff --git a/ghost/admin/app/templates/editor/edit.hbs b/ghost/admin/app/templates/editor/edit.hbs index 86bfdf97fb..0f4b6e3a73 100644 --- a/ghost/admin/app/templates/editor/edit.hbs +++ b/ghost/admin/app/templates/editor/edit.hbs @@ -2,7 +2,6 @@ tagName="section" class="gh-editor gh-view" navIsClosed=navIsClosed - toggleAutoNav=(action "toggleAutoNav") as |editor| }}
@@ -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 - }} +
+ {{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}} +
- {{markdown.pane}} + {{#if markdown.isSplitScreen}} +
+

{{model.titleScratch}}

+
+
+ {{/if}} {{/gh-markdown-editor}} {{!-- TODO: put tool/status bar in here so that scroll area can be fixed --}} diff --git a/ghost/admin/bower.json b/ghost/admin/bower.json index bef3ca0a3b..49b9a34e10 100644 --- a/ghost/admin/bower.json +++ b/ghost/admin/bower.json @@ -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" } } diff --git a/ghost/admin/ember-cli-build.js b/ghost/admin/ember-cli-build.js index 0836084882..4a1cf45d84 100644 --- a/ghost/admin/ember-cli-build.js +++ b/ghost/admin/ember-cli-build.js @@ -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'); diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 33c92438f4..08b83dbdb6 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -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" diff --git a/ghost/admin/yarn.lock b/ghost/admin/yarn.lock index 27732f163a..f758b17d9b 100644 --- a/ghost/admin/yarn.lock +++ b/ghost/admin/yarn.lock @@ -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"