diff --git a/client/coral-admin/src/components/MarkdownEditor.css b/client/coral-admin/src/components/MarkdownEditor.css new file mode 100644 index 000000000..93bf7a9dd --- /dev/null +++ b/client/coral-admin/src/components/MarkdownEditor.css @@ -0,0 +1,622 @@ +$minHeight: 200px; +$fullscreenZIndex: 10; + +.wrapper { + /* Fix blockquote styling of MDL: https://github.com/google/material-design-lite/issues/2037 */ + blockquote { + > p:first-child { + padding-top: 16px; + } + > p:last-child { + margin-bottom: 0; + } + &::after { + margin-left: -0.5em; + } + } +} + +.iconBold { + composes: material-icons from global; + &::before { + content: 'format_bold'; + } +} + +.iconItalic { + composes: material-icons from global; + &::before { + content: 'format_italic'; + } +} + +.iconTitle { + composes: material-icons from global; + &::before { + content: 'title'; + } +} + +.iconQuote { + composes: material-icons from global; + &::before { + content: 'format_quote'; + } +} + +.iconUnorderedList { + composes: material-icons from global; + &::before { + content: 'format_list_bulleted'; + } +} + +.iconOrderedList { + composes: material-icons from global; + &::before { + content: 'format_list_numbered'; + } +} + +.iconLink { + composes: material-icons from global; + &::before { + content: 'link'; + } +} + +.iconImage { + composes: material-icons from global; + &::before { + content: 'insert_photo'; + } +} + +.iconPreview { + composes: material-icons from global; + &::before { + content: 'remove_red_eye'; + } +} + +.iconSideBySide { + composes: material-icons from global; + &::before { + content: 'chrome_reader_mode'; + } +} + +.iconFullscreen { + composes: material-icons from global; + &::before { + content: 'fullscreen'; + } +} + +.iconGuide { + composes: material-icons from global; + &::before { + content: 'help_outline'; + } +} + +/* + * These are modified styles taken from https://github.com/NextStepWebs/simplemde-markdown-editor and + * put through http://sebastianpontow.de/css2compass/. + */ + +/* + * simplemde v1.11.2 + * Copyright Next Step Webs, Inc. + * @link https://github.com/NextStepWebs/simplemde-markdown-editor + * @license MIT + */ +:global { + .CodeMirror { + font-family: monospace; + color: black; + position: relative; + overflow: hidden; + background: white; + height: auto; + min-height: $minHeight; + border: 1px solid #ddd; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + padding: 10px; + font: inherit; + z-index: 1; + pre { + padding: 0 4px; + border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + font-variant-ligatures: none; + } + span { + /*TODO: vertical-align: text-bottom;*/ + } + .CodeMirror-code { + .cm-tag { + color: #63a35c; + } + .cm-attribute { + color: #795da3; + } + .cm-string { + color: #183691; + } + .cm-header-1 { + font-size: 200%; + line-height: 200%; + } + .cm-header-2 { + font-size: 160%; + line-height: 160%; + } + .cm-header-3 { + font-size: 125%; + line-height: 125%; + } + .cm-header-4 { + font-size: 110%; + line-height: 110%; + } + .cm-comment { + background: rgba(0, 0, 0, .05); + border-radius: 2px; + } + .cm-link { + color: #7f8c8d; + } + .cm-url { + color: #aab2b3; + } + .cm-strikethrough { + text-decoration: line-through; + } + .cm-tab { + display: inline-block; + text-decoration: inherit; + } + .CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; + } + .cm-header { + font-weight: bold; + } + .cm-strong { + font-weight: bold; + } + .cm-em { + font-style: italic; + } + .cm-link { + text-decoration: underline; + } + .cm-strikethrough { + text-decoration: line-through; + } + .cm-invalidchar { + color: #f00; + } + } + .CodeMirror-selected { + background: #d9d9d9; + } + .CodeMirror-placeholder { + opacity: .5; + } + div.CodeMirror-secondarycursor { + border-left: 1px solid silver; + } + .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word) { + background: rgba(255, 0, 0, .15); + } + } + .CodeMirror-lines { + padding: 4px 0; + cursor: text; + min-height: 1px; + } + .CodeMirror-scrollbar-filler { + background-color: white; + position: absolute; + z-index: 6; + display: none; + right: 0; + bottom: 0; + } + .CodeMirror-gutter-filler { + background-color: white; + position: absolute; + z-index: 6; + display: none; + left: 0; + bottom: 0; + } + .CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; + position: absolute; + left: 0; + top: 0; + min-height: 100%; + z-index: 3; + box-sizing: content-box; + } + .CodeMirror-guttermarker { + color: black; + } + .CodeMirror-guttermarker-subtle { + color: #999; + } + .CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; + } + .CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; + position: absolute; + } + @keyframes blink { + 0% { + } + 50% { + background-color: transparent; + } + 100% { + } + } + .CodeMirror-scroll { + overflow: scroll !important; + margin-bottom: -30px; + margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; + position: relative; + min-height: $minHeight; + box-sizing: content-box; + } + .CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; + box-sizing: content-box; + } + .CodeMirror-vscrollbar { + position: absolute; + z-index: 6; + display: none; + right: 0; + top: 0; + overflow-x: hidden; + overflow-y: scroll; + } + .CodeMirror-hscrollbar { + position: absolute; + z-index: 6; + display: none; + bottom: 0; + left: 0; + overflow-y: hidden; + overflow-x: scroll; + } + .CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -30px; + *zoom: 1; + *display: inline; + box-sizing: content-box; + } + .CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; + user-select: none; + } + .CodeMirror-gutter-background { + position: absolute; + top: 0; + bottom: 0; + z-index: 4; + } + .CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; + } + .CodeMirror-code { + outline: none; + } + .CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; + pre { + position: static; + } + } + .CodeMirror-focused { + .CodeMirror-selected { + background: #d7d4f0; + } + div.CodeMirror-cursors { + visibility: visible; + } + } + .CodeMirror-selected { + background: #d9d9d9; + } + .CodeMirror-line::selection { + background: #d7d4f0; + } + .CodeMirror-line { + > span::selection { + background: #d7d4f0; + } + > span { + > span::selection { + background: #d7d4f0; + } + } + } + @media print { + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } + } + .CodeMirror-fullscreen { + background: #fff; + position: fixed !important; + top: 50px; + left: 0; + right: 0; + bottom: 0; + height: auto; + z-index: $fullscreenZIndex; + } + .CodeMirror-sided { + width: 50% !important; + } + .editor-toolbar { + position: relative; + opacity: .6; + user-select: none; + padding: 0 10px; + border-top: 1px solid #bbb; + border-left: 1px solid #bbb; + border-right: 1px solid #bbb; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + &:after { + display: block; + content: ' '; + height: 1px; + margin-top: 8px; + } + &:before { + display: block; + content: ' '; + height: 1px; + margin-bottom: 8px; + } + &:hover { + opacity: .8; + } + &.fullscreen { + width: 100%; + height: 50px; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + padding-top: 10px; + padding-bottom: 10px; + box-sizing: border-box; + background: #fff; + border: 0; + position: fixed; + top: 0; + left: 0; + opacity: 1; + z-index: $fullscreenZIndex; + } + &.fullscreen::before { + width: 20px; + height: 50px; + background: linear-gradient(to right, rgba(255, 255, 255, 1) 0, rgba(255, 255, 255, 0) 100%); + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0; + } + &.fullscreen::after { + width: 20px; + height: 50px; + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 1) 100%); + position: fixed; + top: 0; + right: 0; + margin: 0; + padding: 0; + } + a { + display: inline-block; + text-align: center; + text-decoration: none!important; + color: #2c3e50!important; + width: 30px; + height: 30px; + margin: 0; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + outline: 0; + margin-right: 2px; + &.active { + background: #fcfcfc; + border-color: #95a5a6; + } + &:hover { + background: #fcfcfc; + border-color: #95a5a6; + } + &:active { + background: #eee; + } + &:before { + line-height: 30px; + } + } + i.separator { + display: inline-block; + width: 0; + border-left: 1px solid #d9d9d9; + border-right: 1px solid #fff; + color: transparent; + text-indent: -10px; + margin: 0 6px; + } + &.disabled-for-preview a:not(.no-disable) { + pointer-events: none; + background: #fff; + border-color: transparent; + text-shadow: inherit; + } + } + @media only screen and(max-width: 700px) { + .editor-toolbar a.no-mobile { + display: none; + } + } + .editor-statusbar { + padding: 8px 10px; + font-size: 12px; + color: #959694; + text-align: right; + span { + display: inline-block; + min-width: 4em; + margin-left: 1em; + } + .lines:before { + content: 'lines: '; + } + .words:before { + content: 'words: '; + } + .characters:before { + content: 'characters: '; + } + } + .editor-preview { + padding: 10px; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: #fafafa; + z-index: 7; + overflow: auto; + display: none; + box-sizing: border-box; + > p { + margin-top: 0; + } + pre { + background: #eee; + margin-bottom: 10px; + } + table { + td { + border: 1px solid #ddd; + padding: 5px; + } + th { + border: 1px solid #ddd; + padding: 5px; + } + } + } + .editor-preview-side { + padding: 10px; + position: fixed; + bottom: 0; + width: 50%; + top: 50px; + right: 0; + background: #fafafa; + z-index: $fullscreenZIndex; + overflow: auto; + display: none; + box-sizing: border-box; + border: 1px solid #ddd; + > p { + margin-top: 0; + } + pre { + background: #eee; + margin-bottom: 10px; + } + table { + td { + border: 1px solid #ddd; + padding: 5px; + } + th { + border: 1px solid #ddd; + padding: 5px; + } + } + } + .editor-preview-active-side { + display: block; + } + .editor-preview-active { + display: block; + } + .CodeMirror-overwrite .CodeMirror-cursor { + } + .CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; + } + .cm-tab-wrap-hack:after { + content: ''; + } + span.CodeMirror-selectedtext { + background: none; + } + .editor-wrapper input.title { + &:focus { + opacity: .8; + } + &:hover { + opacity: .8; + } + } +} diff --git a/client/coral-admin/src/components/MarkdownEditor.js b/client/coral-admin/src/components/MarkdownEditor.js new file mode 100644 index 000000000..0d2088e51 --- /dev/null +++ b/client/coral-admin/src/components/MarkdownEditor.js @@ -0,0 +1,147 @@ +import React, {Component, PropTypes} from 'react'; +import SimpleMDE from 'simplemde'; +import cn from 'classnames'; +import noop from 'lodash/noop'; +import styles from './MarkdownEditor.css'; + +const config = { + status: false, + + // Do not download fontAwesome icons as we replace them with + // material icons. + autoDownloadFontAwesome: false, + + // Disable built-in spell checker as it is very rudimentary. + spellChecker: false, + + toolbar: [ + { + name: 'bold', + action: SimpleMDE.toggleBold, + className: styles.iconBold, + title: 'Bold', + }, + { + name: 'italic', + action: SimpleMDE.toggleItalic, + className: styles.iconItalic, + title: 'Italic', + }, + { + name: 'title', + action: SimpleMDE.toggleHeadingSmaller, + className: styles.iconTitle, + title: 'Title, Subtitle, Heading', + }, + '|', + { + name: 'quote', + action: SimpleMDE.toggleBlockquote, + className: styles.iconQuote, + title: 'Quote', + }, + { + name: 'unordered-list', + action: SimpleMDE.toggleUnorderedList, + className: styles.iconUnorderedList, + title: 'Generic List', + }, + { + name: 'ordered-list', + action: SimpleMDE.toggleOrderedList, + className: styles.iconOrderedList, + title: 'Numbered List', + }, + '|', + { + name: 'link', + action: SimpleMDE.drawLink, + className: styles.iconLink, + title: 'Create Link', + }, + { + name: 'image', + action: SimpleMDE.drawImage, + className: styles.iconImage, + title: 'Insert Image', + }, + '|', + { + name: 'preview', + action: SimpleMDE.togglePreview, + className: cn(styles.iconPreview, 'no-disable'), + title: 'Toggle Preview', + }, + { + name: 'side-by-side', + action: SimpleMDE.toggleSideBySide, + className: cn(styles.iconSideBySide, 'no-disable'), + title: 'Toggle Side by Side', + }, + { + name: 'fullscreen', + action: SimpleMDE.toggleFullScreen, + className: cn(styles.iconFullscreen, 'no-disable'), + title: 'Toggle Fullscreen', + }, + '|', + { + name: 'guide', + action: 'https://simplemde.com/markdown-guide', + className: styles.iconGuide, + title: 'Markdown Guide', + }, + ], +}; + +export default class MarkdownEditor extends Component { + + textarea = null + editor = null + + onRef = (ref) => this.textarea = ref + + componentDidMount() { + this.editor = new SimpleMDE({ + ...config, + element: this.textarea, + }); + this.editor.codemirror.on('change', this.onChange); + } + + componentWillReceiveProps(nextProps) { + if (this.props.value !== nextProps.value && nextProps.value !== this.editor.value()) { + this.editor.value(nextProps.value); + } + } + + componentDidUpdate() { + + // Workaround empty render issue. + // https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313 + this.editor.codemirror.refresh(); + } + + componentWillUnmount() { + this.editor.toTextArea(); + } + + onChange = () => { + if (this.props.onChange) { + this.props.onChange(this.editor.value()); + } + } + + render() { + return ( +
+