diff --git a/ghost/admin/app/components/gh-ed-editor.js b/ghost/admin/app/components/gh-ed-editor.js deleted file mode 100644 index a5be4948ec..0000000000 --- a/ghost/admin/app/components/gh-ed-editor.js +++ /dev/null @@ -1,51 +0,0 @@ -import TextArea from 'ember-components/text-area'; -import run from 'ember-runloop'; -import EditorAPI from 'ghost-admin/mixins/ed-editor-api'; -import EditorShortcuts from 'ghost-admin/mixins/ed-editor-shortcuts'; -import EditorScroll from 'ghost-admin/mixins/ed-editor-scroll'; -import {InvokeActionMixin} from 'ember-invoke-action'; - -export default TextArea.extend(EditorAPI, EditorShortcuts, EditorScroll, InvokeActionMixin, { - focus: false, - - /** - * Tell the controller about focusIn events, will trigger an autosave on a new document - */ - focusIn() { - this.sendAction('onFocusIn'); - }, - - /** - * Sets the focus of the textarea if needed - */ - setFocus() { - if (this.get('focus')) { - this.$().val(this.$().val()).focus(); - } - }, - - /** - * Sets up properties at render time - */ - didInsertElement() { - this._super(...arguments); - - this.setFocus(); - - this.invokeAction('setEditor', this); - - run.scheduleOnce('afterRender', this, this.afterRenderEvent); - }, - - afterRenderEvent() { - if (this.get('focus') && this.get('focusCursorAtEnd')) { - this.setSelection('end'); - } - }, - - actions: { - toggleCopyHTMLModal(generatedHTML) { - this.invokeAction('toggleCopyHTMLModal', generatedHTML); - } - } -}); diff --git a/ghost/admin/app/components/gh-ed-preview.js b/ghost/admin/app/components/gh-ed-preview.js deleted file mode 100644 index 625566e6d8..0000000000 --- a/ghost/admin/app/components/gh-ed-preview.js +++ /dev/null @@ -1,127 +0,0 @@ -import Ember from 'ember'; -import Component from 'ember-component'; -import EmberObject from 'ember-object'; -import run from 'ember-runloop'; -import {A as emberA} from 'ember-array/utils'; -import {formatMarkdown} from 'ghost-admin/helpers/gh-format-markdown'; - -// ember-cli-shims doesn't export uuid -const {uuid} = Ember; - -export default Component.extend({ - _scrollWrapper: null, - - previewHTML: '', - - init() { - this._super(...arguments); - this.set('imageUploadComponents', emberA([])); - this.buildPreviewHTML(); - }, - - didInsertElement() { - this._super(...arguments); - this._scrollWrapper = this.$().closest('.entry-preview-content'); - this.adjustScrollPosition(this.get('scrollPosition')); - }, - - didReceiveAttrs(attrs) { - this._super(...arguments); - - if (!attrs.oldAttrs) { - return; - } - - if (attrs.newAttrs.scrollPosition && attrs.newAttrs.scrollPosition.value !== attrs.oldAttrs.scrollPosition.value) { - this.adjustScrollPosition(attrs.newAttrs.scrollPosition.value); - } - - if (attrs.newAttrs.markdown.value !== attrs.oldAttrs.markdown.value) { - run.throttle(this, this.buildPreviewHTML, 30, false); - } - }, - - adjustScrollPosition(scrollPosition) { - let scrollWrapper = this._scrollWrapper; - - if (scrollWrapper) { - scrollWrapper.scrollTop(scrollPosition); - } - }, - - buildPreviewHTML() { - let markdown = this.get('markdown'); - let html = formatMarkdown([markdown]).string; - let template = document.createElement('template'); - template.innerHTML = html; - let fragment = template.content; - - if (!fragment) { - fragment = document.createDocumentFragment(); - - while (template.childNodes[0]) { - fragment.appendChild(template.childNodes[0]); - } - } - - let dropzones = fragment.querySelectorAll('.js-drop-zone'); - let components = this.get('imageUploadComponents'); - - if (dropzones.length !== components.length) { - components = emberA([]); - this.set('imageUploadComponents', components); - } - - [...dropzones].forEach((oldEl, i) => { - let el = oldEl.cloneNode(true); - let component = components[i]; - let uploadTarget = el.querySelector('.js-upload-target'); - let altTextWrapper = oldEl.querySelector('.js-drop-zone .description strong'); - let id = uuid(); - let destinationElementId = `image-uploader-${id}`; - let src, altText; - - if (uploadTarget) { - src = uploadTarget.getAttribute('src'); - } - - if (altTextWrapper) { - altText = altTextWrapper.innerHTML; - } - - if (component) { - component.set('destinationElementId', destinationElementId); - component.set('src', src); - component.set('altText', altText); - } else { - let imageUpload = EmberObject.create({ - destinationElementId, - id, - src, - altText, - index: i - }); - - this.get('imageUploadComponents').pushObject(imageUpload); - } - - el.id = destinationElementId; - el.innerHTML = ''; - el.classList.remove('image-uploader'); - - oldEl.parentNode.replaceChild(el, oldEl); - }); - - this.set('previewHTML', fragment); - }, - - actions: { - updateImageSrc(index, url) { - this.attrs.updateImageSrc(index, url); - }, - - updateHeight() { - this.attrs.updateHeight(this.$().height()); - } - } -}); diff --git a/ghost/admin/app/components/gh-editor.js b/ghost/admin/app/components/gh-editor.js deleted file mode 100644 index 9165d6d8d5..0000000000 --- a/ghost/admin/app/components/gh-editor.js +++ /dev/null @@ -1,123 +0,0 @@ -import Component from 'ember-component'; -import computed, {equal} from 'ember-computed'; -import run from 'ember-runloop'; - -import ShortcutsMixin from 'ghost-admin/mixins/shortcuts'; -import imageManager from 'ghost-admin/utils/ed-image-manager'; -import editorShortcuts from 'ghost-admin/utils/editor-shortcuts'; -import {invokeAction} from 'ember-invoke-action'; - -export default Component.extend(ShortcutsMixin, { - tagName: 'section', - classNames: ['view-container', 'view-editor'], - - activeTab: 'markdown', - editor: null, - editorDisabled: undefined, - editorScrollInfo: null, // updated when gh-ed-editor component scrolls - height: null, // updated when markdown is rendered - shouldFocusEditor: false, - showCopyHTMLModal: false, - copyHTMLModalContent: null, - - shortcuts: editorShortcuts, - - markdownActive: equal('activeTab', 'markdown'), - previewActive: equal('activeTab', 'preview'), - - // HTML Preview listens to scrollPosition and updates its scrollTop value - // This property receives scrollInfo from the textEditor, and height from the preview pane, and will update the - // scrollPosition value such that when either scrolling or typing-at-the-end of the text editor the preview pane - // stays in sync - scrollPosition: computed('editorScrollInfo', 'height', function () { - let scrollInfo = this.get('editorScrollInfo'); - let {$previewContent, $previewViewPort} = this; - - if (!scrollInfo || !$previewContent || !$previewViewPort) { - return 0; - } - - let previewHeight = $previewContent.height() - $previewViewPort.height(); - let previewPosition, ratio; - - ratio = previewHeight / scrollInfo.diff; - previewPosition = scrollInfo.top * ratio; - - return previewPosition; - }), - - didInsertElement() { - this._super(...arguments); - this.registerShortcuts(); - run.scheduleOnce('afterRender', this, this._cacheElements); - }, - - willDestroyElement() { - invokeAction(this, 'onTeardown'); - - this.removeShortcuts(); - }, - - _cacheElements() { - // cache these elements for use in other methods - this.$previewViewPort = this.$('.js-entry-preview-content'); - this.$previewContent = this.$('.js-rendered-markdown'); - }, - - actions: { - selectTab(tab) { - this.set('activeTab', tab); - }, - - updateScrollInfo(scrollInfo) { - this.set('editorScrollInfo', scrollInfo); - }, - - updateHeight(height) { - this.set('height', height); - }, - - // set from a `sendAction` on the gh-ed-editor component, - // so that we get a reference for handling uploads. - setEditor(editor) { - this.set('editor', editor); - }, - - disableEditor() { - this.set('editorDisabled', true); - }, - - enableEditor() { - this.set('editorDisabled', undefined); - }, - - // The actual functionality is implemented in utils/ed-editor-shortcuts - editorShortcut(options) { - if (this.editor.$().is(':focus')) { - this.editor.shortcut(options.type); - } - }, - - // Match the uploaded file to a line in the editor, and update that line with a path reference - // ensuring that everything ends up in the correct place and format. - handleImgUpload(imageIndex, newSrc) { - let editor = this.get('editor'); - let editorValue = editor.getValue(); - let replacement = imageManager.getSrcRange(editorValue, imageIndex); - let cursorPosition; - - if (replacement) { - cursorPosition = replacement.start + newSrc.length + 1; - if (replacement.needsParens) { - newSrc = `(${newSrc})`; - } - editor.replaceSelection(newSrc, replacement.start, replacement.end, cursorPosition); - } - }, - - toggleCopyHTMLModal(generatedHTML) { - this.set('copyHTMLModalContent', generatedHTML); - this.toggleProperty('showCopyHTMLModal'); - } - } -}); diff --git a/ghost/admin/app/mixins/ed-editor-api.js b/ghost/admin/app/mixins/ed-editor-api.js deleted file mode 100644 index ec3a7a5d1f..0000000000 --- a/ghost/admin/app/mixins/ed-editor-api.js +++ /dev/null @@ -1,138 +0,0 @@ -import Mixin from 'ember-metal/mixin'; -import run from 'ember-runloop'; - -export default Mixin.create({ - /** - * Get Value - * - * Get the full contents of the textarea - * - * @returns {String} - */ - getValue() { - return this.readDOMAttr('value'); - }, - - /** - * Get Selection - * - * Return the currently selected text from the textarea - * - * @returns {Selection} - */ - getSelection() { - return this.$().getSelection(); - }, - - /** - * Get Line To Cursor - * - * Fetch the string of characters from the start of the given line up to the cursor - * @returns {{text: string, start: number}} - */ - getLineToCursor() { - let selection = this.$().getSelection(); - let value = this.getValue(); - let lineStart; - - // Normalise newlines - value = value.replace('\r\n', '\n'); - - // We want to look at the characters behind the cursor - lineStart = value.lastIndexOf('\n', selection.start - 1) + 1; - - return { - text: value.substring(lineStart, selection.start), - start: lineStart - }; - }, - - /** - * Get Line - * - * Return the string of characters for the line the cursor is currently on - * - * @returns {{text: string, start: number, end: number}} - */ - getLine() { - let selection = this.$().getSelection(); - let value = this.getValue(); - let lineStart, - lineEnd; - - // Normalise newlines - value = value.replace('\r\n', '\n'); - - // We want to look at the characters behind the cursor - lineStart = value.lastIndexOf('\n', selection.start - 1) + 1; - lineEnd = value.indexOf('\n', selection.start); - lineEnd = lineEnd === -1 ? value.length - 1 : lineEnd; - - return { - // jscs:disable - text: value.substring(lineStart, lineEnd).replace(/^\n/, ''), - // jscs:enable - start: lineStart, - end: lineEnd - }; - }, - - /** - * Set Selection - * - * Set the section of text in the textarea that should be selected by the cursor - * - * @param {number} start - * @param {number} end - */ - setSelection(start, end) { - let $textarea = this.$(); - - if (start === 'end') { - start = $textarea.val().length; - } - - end = end || start; - - $textarea.setSelection(start, end); - }, - - /** - * Replace Selection - * - * @param {String} replacement - the string to replace with - * @param {number} replacementStart - where to start replacing - * @param {number} [replacementEnd] - when to stop replacing, defaults to replacementStart - * @param {String|boolean|Object} [cursorPosition] - where to put the cursor after replacing - * - * Cursor position after replacement defaults to the end of the replacement. - * Providing selectionStart only will cause the cursor to be placed there, or alternatively a range can be selected - * by providing selectionEnd. - */ - replaceSelection(replacement, replacementStart, replacementEnd, cursorPosition) { - run.schedule('afterRender', this, function () { - let $textarea = this.$(); - - cursorPosition = cursorPosition || 'collapseToEnd'; - replacementEnd = replacementEnd || replacementStart; - - $textarea.setSelection(replacementStart, replacementEnd); - - if (['select', 'collapseToStart', 'collapseToEnd'].indexOf(cursorPosition) !== -1) { - $textarea.replaceSelectedText(replacement, cursorPosition); - } else { - $textarea.replaceSelectedText(replacement); - if (cursorPosition.hasOwnProperty('start')) { - $textarea.setSelection(cursorPosition.start, cursorPosition.end); - } else { - $textarea.setSelection(cursorPosition, cursorPosition); - } - } - - $textarea.focus(); - // Tell the editor it has changed, as programmatic replacements won't trigger this automatically - this._elementValueDidChange(); - this.sendAction('onChange'); - }); - } -}); diff --git a/ghost/admin/app/mixins/ed-editor-scroll.js b/ghost/admin/app/mixins/ed-editor-scroll.js deleted file mode 100644 index cdf5555965..0000000000 --- a/ghost/admin/app/mixins/ed-editor-scroll.js +++ /dev/null @@ -1,99 +0,0 @@ -import Mixin from 'ember-metal/mixin'; -import run from 'ember-runloop'; -import {invokeAction} from 'ember-invoke-action'; - -export default Mixin.create({ - /** - * Determine if the cursor is at the end of the textarea - */ - isCursorAtEnd() { - let selection = this.$().getSelection(); - let value = this.getValue(); - let linesAtEnd = 3; - let match, - stringAfterCursor; - - stringAfterCursor = value.substring(selection.end); - match = stringAfterCursor.match(/\n/g); - - if (!match || match.length < linesAtEnd) { - return true; - } - - return false; - }, - - /** - * Build an object that represents the scroll state - */ - getScrollInfo() { - let scroller = this.get('element'); - let scrollInfo = { - top: scroller.scrollTop, - height: scroller.scrollHeight, - clientHeight: scroller.clientHeight, - diff: scroller.scrollHeight - scroller.clientHeight, - padding: 50, - isCursorAtEnd: this.isCursorAtEnd() - }; - - return scrollInfo; - }, - - /** - * Calculate if we're within scrollInfo.padding of the end of the document, and scroll the rest of the way - */ - adjustScrollPosition() { - // If we're receiving change events from the end of the document, i.e the user is typing-at-the-end, update the - // scroll position to ensure both panels stay in view and in sync - let scrollInfo = this.getScrollInfo(); - - if (scrollInfo.isCursorAtEnd && (scrollInfo.diff >= scrollInfo.top) && - (scrollInfo.diff < scrollInfo.top + scrollInfo.padding)) { - scrollInfo.top += scrollInfo.padding; - // Scroll the left pane - this.$().scrollTop(scrollInfo.top); - } - }, - - /** - * Send the scrollInfo for scrollEvents to the view so that the preview pane can be synced - */ - scrollHandler() { - this.set('scrollThrottle', run.throttle(this, () => { - invokeAction(this, 'updateScrollInfo', this.getScrollInfo()); - }, 10)); - }, - - /** - * once the element is in the DOM bind to the events which control scroll behaviour - */ - attachScrollHandlers() { - let $el = this.$(); - - $el.on('keypress', run.bind(this, this.adjustScrollPosition)); - - $el.on('scroll', run.bind(this, this.scrollHandler)); - }, - - /** - * once the element has been removed from the DOM unbind from the events which control scroll behaviour - */ - detachScrollHandlers() { - this.$().off('keypress'); - this.$().off('scroll'); - run.cancel(this.get('scrollThrottle')); - }, - - didInsertElement() { - this._super(...arguments); - - this.attachScrollHandlers(); - }, - - willDestroyElement() { - this._super(...arguments); - - this.detachScrollHandlers(); - } -}); diff --git a/ghost/admin/app/mixins/ed-editor-shortcuts.js b/ghost/admin/app/mixins/ed-editor-shortcuts.js deleted file mode 100644 index 5e87f13ea7..0000000000 --- a/ghost/admin/app/mixins/ed-editor-shortcuts.js +++ /dev/null @@ -1,171 +0,0 @@ -/* global moment, Showdown */ -import Mixin from 'ember-metal/mixin'; -import titleize from 'ghost-admin/utils/titleize'; - -// Used for simple, noncomputational replace-and-go! shortcuts. -// See default case in shortcut function below. -let simpleShortcutSyntax = { - bold: { - regex: '**|**', - cursor: '|' - }, - italic: { - regex: '*|*', - cursor: '|' - - }, - strike: { - regex: '~~|~~', - cursor: '|' - }, - code: { - regex: '`|`', - cursor: '|' - }, - blockquote: { - regex: '> |', - cursor: '|', - newline: true - }, - list: { - regex: '* |', - cursor: '|', - newline: true - }, - link: { - regex: '[|](http://)', - cursor: 'http://' - }, - image: { - regex: '![|](http://)', - cursor: 'http://', - newline: true - } -}; - -let shortcuts = { - simple(type, replacement, selection, line) { - let startIndex = 0; - let shortcut; - - if (simpleShortcutSyntax.hasOwnProperty(type)) { - shortcut = simpleShortcutSyntax[type]; - // insert the markdown - replacement.text = shortcut.regex.replace('|', selection.text); - - // add a newline if needed - if (shortcut.newline && line.text !== '') { - startIndex = 1; - replacement.text = `\n${replacement.text}`; - } - - // handle cursor position - if (selection.text === '' && shortcut.cursor === '|') { - // the cursor should go where | was - replacement.position = startIndex + replacement.start + shortcut.regex.indexOf(shortcut.cursor); - } else if (shortcut.cursor !== '|') { - // the cursor should select the string which matches shortcut.cursor - replacement.position = { - start: replacement.start + replacement.text.indexOf(shortcut.cursor) - }; - replacement.position.end = replacement.position.start + shortcut.cursor.length; - } - } - - return replacement; - }, - - cycleHeaderLevel(replacement, line) { - let match = line.text.match(/^#+/); - let currentHeaderLevel, - hashPrefix; - - if (!match) { - currentHeaderLevel = 1; - } else { - currentHeaderLevel = match[0].length; - } - - if (currentHeaderLevel > 2) { - currentHeaderLevel = 1; - } - - hashPrefix = new Array(currentHeaderLevel + 2).join('#'); - - replacement.text = `${hashPrefix} ${line.text.replace(/^#* /, '')}`; - - replacement.start = line.start; - replacement.end = line.end; - - return replacement; - }, - - copyHTML(editor, selection) { - let converter = new Showdown.converter(); - let generatedHTML; - - if (selection.text) { - generatedHTML = converter.makeHtml(selection.text); - } else { - generatedHTML = converter.makeHtml(editor.getValue()); - } - - // Talk to the editor - editor.send('toggleCopyHTMLModal', generatedHTML); - }, - - currentDate(replacement) { - replacement.text = moment(new Date()).format('D MMMM YYYY'); - return replacement; - }, - - uppercase(replacement, selection) { - replacement.text = selection.text.toLocaleUpperCase(); - return replacement; - }, - - lowercase(replacement, selection) { - replacement.text = selection.text.toLocaleLowerCase(); - return replacement; - }, - - titlecase(replacement, selection) { - replacement.text = titleize(selection.text); - return replacement; - } -}; - -export default Mixin.create({ - shortcut(type) { - let selection = this.getSelection(); - let replacement = { - start: selection.start, - end: selection.end, - position: 'collapseToEnd' - }; - - switch (type) { - // This shortcut is special as it needs to send an action - case 'copyHTML': - shortcuts.copyHTML(this, selection); - break; - case 'cycleHeaderLevel': - replacement = shortcuts.cycleHeaderLevel(replacement, this.getLine()); - break; - // These shortcuts all process the basic information - case 'currentDate': - case 'uppercase': - case 'lowercase': - case 'titlecase': - replacement = shortcuts[type](replacement, selection, this.getLineToCursor()); - break; - // All the of basic formatting shortcuts work with a regex - default: - replacement = shortcuts.simple(type, replacement, selection, this.getLineToCursor()); - } - - if (replacement.text) { - this.replaceSelection(replacement.text, replacement.start, replacement.end, replacement.position); - } - } -}); diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css index 8abaab53c4..3d45001e76 100644 --- a/ghost/admin/app/styles/layouts/editor.css +++ b/ghost/admin/app/styles/layouts/editor.css @@ -32,229 +32,6 @@ } -/* Container & Headers -/* ---------------------------------------------------------- */ - -.view-editor { - display: flex; -} - -.editor .entry-preview { - border-left: #dfe1e3 1px solid; -} - -.editor .entry-markdown, -.editor .entry-preview { - position: relative; /*TODO: Remove*/ - display: flex; - flex-direction: column; - width: 50%; -} - -/* Content areas at the top, and fill available space */ -.editor .entry-markdown-content, -.editor .entry-preview-content { - order: 1; - flex-grow: 1; -} - -/* Headers at the bottom, and fixed height */ -.editor .floatingheader { - order: 2; - flex-shrink: 0; - display: flex; - justify-content: space-between; - align-items: center; - padding: 5px 15px; - height: 40px; - border-top: #dfe1e3 1px solid; - color: var(--midgrey); - font-size: 1.2rem; - line-height: 1em; -} -.editor .floatingheader a { - padding: 5px 15px; - color: var(--midgrey); -} -.editor .floatingheader a.active { - font-weight: bold; -} -.editor .floatingheader a:first-of-type { - padding-left: 0; -} -.editor .floatingheader a:last-of-type { - padding-right: 0; -} -.editor .floatingheader span a:not(:first-of-type) { - border-left: 1px solid #dfe1e3; -} -.editor .floatingheader .mobile-tabs { - display: none; -} - -/* Switch to 1 col editor on small screens */ -@media (max-width: 1000px) { - .editor .entry-markdown, - .editor .entry-preview { - width: 100%; - border-left: none; - } - /* We can't use display:none here as we want to keep widths/heights - * so that scrolling is kept in sync */ - .editor .entry-markdown:not(.active), - .editor .entry-preview:not(.active) { - visibility: hidden; - position: absolute; - z-index: -1; - height: 100%; - } - .editor .floatingheader .mobile-tabs { - display: inline; - } - .editor .floatingheader .desktop-tabs { - display: none; - } -} - - -/* Editor (Left pane) -/* ---------------------------------------------------------- */ - -.editor .entry-markdown-content { - position: relative; - flex-grow: 1; -} - -.editor .markdown-editor { - /* Legacy absolute positioning */ - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - overflow: auto; - -webkit-overflow-scrolling: touch; - padding: 21px 20px 36px 20px; - max-width: 100%; - height: 100%; - border: 0; - color: color(var(--darkgrey) lightness(+10%)); - font-family: var(--font-family-mono); - font-size: 1.6rem; - line-height: 2.5rem; - resize: none; -} - -.editor .markdown-editor:focus { - outline: 0; -} - -@media (max-width: 450px) { - .editor .markdown-editor { - padding: 15px; - } -} - - -/* FFF: Fucking Firefox Fixes -/* ---------------------------------------------------------- */ - -@-moz-document url-prefix() { - .editor .markdown-editor { - top: 40px; - padding-top: 0; - padding-bottom: 0; - height: calc(100% - 40px); - } -} - - -/* Preview (Right pane) -/* ---------------------------------------------------------- */ - -.editor .entry-preview-content { - flex-grow: 1; - overflow: auto; - -webkit-overflow-scrolling: touch; - padding: 19px 20px 37px 20px; - word-break: break-word; - hyphens: auto; - cursor: default; -} - -/* The styles for the actual content inside the preview */ -.entry-preview-content, -.content-preview-content { - font-size: 1.8rem; - line-height: 1.5em; - font-weight: 200; -} - -.entry-preview-content *, -.content-preview-content * { - user-select: text; -} - -.entry-preview-content a, -.content-preview-content a { - color: var(--blue); - text-decoration: underline; -} - -.entry-preview-content sup a, -.content-preview-content sup a { - text-decoration: none; -} - -.entry-preview-content .btn, -.content-preview-content .btn { - color: #dfe1e3; - text-decoration: none; -} - -.entry-preview-content .img-placeholder, -.content-preview-content .img-placeholder { - position: relative; - height: 100px; - border: 5px dashed #dfe1e3; -} - -.entry-preview-content .img-placeholder span, -.content-preview-content .img-placeholder span { - position: absolute; - top: 50%; - display: block; - margin-top: -15px; - width: 100%; - height: 30px; - text-align: center; -} - -.entry-preview-content a.image-edit, -.content-preview-content a.image-edit { - width: 16px; - height: 16px; -} - -.entry-preview-content img, -.content-preview-content img { - margin: 0 auto; - max-width: 100%; - height: auto; -} - -/* Placeholder objects for