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"