From c9ee92d811cc9acd8032aa8222c785f05b63e1fe Mon Sep 17 00:00:00 2001 From: ddsuhaimi Date: Tue, 30 Mar 2021 23:26:53 +0700 Subject: [PATCH 01/10] initial setup for rich text editor --- client/package.json | 1 + .../RichTextEditor.component.jsx | 228 ++++++++++++++++++ .../RichTextEditor/RichTextEditor.styles.scss | 71 ++++++ .../src/components/RichTextEditor/draft.css | 10 + .../PostForm/AskForm/AskForm.component.jsx | 8 +- 5 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 client/src/components/RichTextEditor/RichTextEditor.component.jsx create mode 100644 client/src/components/RichTextEditor/RichTextEditor.styles.scss create mode 100644 client/src/components/RichTextEditor/draft.css diff --git a/client/package.json b/client/package.json index dab578f..4cf04f7 100644 --- a/client/package.json +++ b/client/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "axios": "^0.21.1", + "draft-js": "^0.11.7", "moment": "^2.27.0", "node-sass": "^4.14.1", "prop-types": "^15.7.2", diff --git a/client/src/components/RichTextEditor/RichTextEditor.component.jsx b/client/src/components/RichTextEditor/RichTextEditor.component.jsx new file mode 100644 index 0000000..195a3da --- /dev/null +++ b/client/src/components/RichTextEditor/RichTextEditor.component.jsx @@ -0,0 +1,228 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {Editor, EditorState, RichUtils, getDefaultKeyBinding} from 'draft-js'; +import './draft.css'; +// import './rich-editor.css'; +import './RichTextEditor.styles.scss'; +// const RichTextEditor = () => { +// const [editorState, setEditorState] = React.useState(() => +// EditorState.createEmpty() +// ); + +// const editor = React.useRef(null); +// function focusEditor() { +// editor.current.focus(); +// } +// return ( +//
+// +//
+// ); +// }; + +// export default RichTextEditor; + +class RichTextEditor extends React.Component { + constructor(props) { + super(props); + this.state = {editorState: EditorState.createEmpty()}; + + this.focus = () => this.refs.editor.focus(); + this.onChange = (editorState) => this.setState({editorState}); + + this.handleKeyCommand = this._handleKeyCommand.bind(this); + this.mapKeyToEditorCommand = this._mapKeyToEditorCommand.bind(this); + this.toggleBlockType = this._toggleBlockType.bind(this); + this.toggleInlineStyle = this._toggleInlineStyle.bind(this); + } + + _handleKeyCommand(command, editorState) { + const newState = RichUtils.handleKeyCommand(editorState, command); + if (newState) { + this.onChange(newState); + return true; + } + return false; + } + + _mapKeyToEditorCommand(e) { + if (e.keyCode === 9 /* TAB */) { + const newEditorState = RichUtils.onTab( + e, + this.state.editorState, + 4 /* maxDepth */ + ); + if (newEditorState !== this.state.editorState) { + this.onChange(newEditorState); + } + return; + } + return getDefaultKeyBinding(e); + } + + _toggleBlockType(blockType) { + this.onChange(RichUtils.toggleBlockType(this.state.editorState, blockType)); + } + + _toggleInlineStyle(inlineStyle) { + this.onChange( + RichUtils.toggleInlineStyle(this.state.editorState, inlineStyle) + ); + } + + render() { + const {editorState} = this.state; + + // If the user changes block type before entering any text, we can + // either style the placeholder or hide it. Let's just hide it now. + let className = 'RichEditor-editor'; + var contentState = editorState.getCurrentContent(); + if (!contentState.hasText()) { + if (contentState.getBlockMap().first().getType() !== 'unstyled') { + className += ' RichEditor-hidePlaceholder'; + } + } + + return ( +
+ + +
+ +
+
+ ); + } +} + +// Custom overrides for "code" style. +const styleMap = { + CODE: { + backgroundColor: 'rgba(0, 0, 0, 0.05)', + fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace', + fontSize: 16, + padding: 2, + }, +}; + +function getBlockStyle(block) { + switch (block.getType()) { + case 'blockquote': + return 'RichEditor-blockquote'; + default: + return null; + } +} + +class StyleButton extends React.Component { + constructor() { + super(); + this.onToggle = (e) => { + e.preventDefault(); + this.props.onToggle(this.props.style); + }; + } + + render() { + let className = 'RichEditor-styleButton'; + if (this.props.active) { + className += ' RichEditor-activeButton'; + } + + return ( + + {this.props.label} + + ); + } +} + +const BLOCK_TYPES = [ + /** + * In case the heading is needed, comment these out + */ + // {label: 'H1', style: 'header-one'}, + // {label: 'H2', style: 'header-two'}, + // {label: 'H3', style: 'header-three'}, + // {label: 'H4', style: 'header-four'}, + // {label: 'H5', style: 'header-five'}, + // {label: 'H6', style: 'header-six'}, + {label: 'Blockquote', style: 'blockquote'}, + {label: 'UL', style: 'unordered-list-item'}, + {label: 'OL', style: 'ordered-list-item'}, + {label: 'Code Block', style: 'code-block'}, +]; + +const BlockStyleControls = (props) => { + const {editorState} = props; + const selection = editorState.getSelection(); + const blockType = editorState + .getCurrentContent() + .getBlockForKey(selection.getStartKey()) + .getType(); + + return ( +
+ {BLOCK_TYPES.map((type) => ( + + ))} +
+ ); +}; + +var INLINE_STYLES = [ + {label: 'Bold', style: 'BOLD'}, + {label: 'Italic', style: 'ITALIC'}, + {label: 'Underline', style: 'UNDERLINE'}, + {label: 'Monospace', style: 'CODE'}, +]; + +const InlineStyleControls = (props) => { + const currentStyle = props.editorState.getCurrentInlineStyle(); + + return ( +
+ {INLINE_STYLES.map((type) => ( + + ))} +
+ ); +}; + +export default RichTextEditor; diff --git a/client/src/components/RichTextEditor/RichTextEditor.styles.scss b/client/src/components/RichTextEditor/RichTextEditor.styles.scss new file mode 100644 index 0000000..674205d --- /dev/null +++ b/client/src/components/RichTextEditor/RichTextEditor.styles.scss @@ -0,0 +1,71 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + .RichEditor-root { + /* background: rgb(167, 165, 165); */ + /* border: 1px solid #ddd; */ + // font-family: 'Georgia', serif; + font-size: 14px; + /* padding: 15px; */ + } + + .RichEditor-editor { + border-top: 1px solid #ddd; + cursor: text; + font-size: 14px; + margin-top: 10px; + } + + .RichEditor-editor .public-DraftEditorPlaceholder-root, + .RichEditor-editor .public-DraftEditor-content { + margin: 0 -15px -15px; + padding: 15px; + } + + .RichEditor-editor .public-DraftEditor-content { + min-height: 100px; + } + + .RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root { + display: none; + } + + .RichEditor-editor .RichEditor-blockquote { + border-left: 5px solid #eee; + color: #666; + font-family: 'Hoefler Text', 'Georgia', serif; + font-style: italic; + margin: 16px 0; + padding: 10px 20px; + } + + .RichEditor-editor .public-DraftStyleDefault-pre { + background-color: rgba(0, 0, 0, 0.05); + font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace; + font-size: 16px; + padding: 20px; + } + + .RichEditor-controls { + font-family: 'Helvetica', sans-serif; + font-size: 14px; + margin-bottom: 5px; + user-select: none; + } + + .RichEditor-styleButton { + color: #999; + cursor: pointer; + margin-right: 16px; + padding: 2px 0; + display: inline-block; + } + + .RichEditor-activeButton { + color: #5890ff; + } + \ No newline at end of file diff --git a/client/src/components/RichTextEditor/draft.css b/client/src/components/RichTextEditor/draft.css new file mode 100644 index 0000000..0cf5703 --- /dev/null +++ b/client/src/components/RichTextEditor/draft.css @@ -0,0 +1,10 @@ +/** + * Draft v0.11.3 + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.DraftEditor-editorContainer,.DraftEditor-root,.public-DraftEditor-content{height:inherit;text-align:initial}.public-DraftEditor-content[contenteditable=true]{-webkit-user-modify:read-write-plaintext-only}.DraftEditor-root{position:relative}.DraftEditor-editorContainer{background-color:rgba(255,255,255,0);border-left:.1px solid transparent;position:relative;z-index:1}.public-DraftEditor-block{position:relative}.DraftEditor-alignLeft .public-DraftStyleDefault-block{text-align:left}.DraftEditor-alignLeft .public-DraftEditorPlaceholder-root{left:0;text-align:left}.DraftEditor-alignCenter .public-DraftStyleDefault-block{text-align:center}.DraftEditor-alignCenter .public-DraftEditorPlaceholder-root{margin:0 auto;text-align:center;width:100%}.DraftEditor-alignRight .public-DraftStyleDefault-block{text-align:right}.DraftEditor-alignRight .public-DraftEditorPlaceholder-root{right:0;text-align:right}.public-DraftEditorPlaceholder-root{color:#9197a3;position:absolute;z-index:1}.public-DraftEditorPlaceholder-hasFocus{color:#bdc1c9}.DraftEditorPlaceholder-hidden{display:none}.public-DraftStyleDefault-block{position:relative;white-space:pre-wrap}.public-DraftStyleDefault-ltr{direction:ltr;text-align:left}.public-DraftStyleDefault-rtl{direction:rtl;text-align:right}.public-DraftStyleDefault-listLTR{direction:ltr}.public-DraftStyleDefault-listRTL{direction:rtl}.public-DraftStyleDefault-ol,.public-DraftStyleDefault-ul{margin:16px 0;padding:0}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR{margin-left:1.5em}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL{margin-right:1.5em}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR{margin-left:3em}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL{margin-right:3em}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR{margin-left:4.5em}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL{margin-right:4.5em}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR{margin-left:6em}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL{margin-right:6em}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR{margin-left:7.5em}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL{margin-right:7.5em}.public-DraftStyleDefault-unorderedListItem{list-style-type:square;position:relative}.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0{list-style-type:disc}.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1{list-style-type:circle}.public-DraftStyleDefault-orderedListItem{list-style-type:none;position:relative}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before{left:-36px;position:absolute;text-align:right;width:30px}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before{position:absolute;right:-36px;text-align:left;width:30px}.public-DraftStyleDefault-orderedListItem:before{content:counter(ol0) ". ";counter-increment:ol0}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before{content:counter(ol1,lower-alpha) ". ";counter-increment:ol1}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before{content:counter(ol2,lower-roman) ". ";counter-increment:ol2}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before{content:counter(ol3) ". ";counter-increment:ol3}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before{content:counter(ol4,lower-alpha) ". ";counter-increment:ol4}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset{counter-reset:ol0}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset{counter-reset:ol1}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset{counter-reset:ol2}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset{counter-reset:ol3}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset{counter-reset:ol4} diff --git a/client/src/pages/PostForm/AskForm/AskForm.component.jsx b/client/src/pages/PostForm/AskForm/AskForm.component.jsx index ba947e8..e5a0a54 100644 --- a/client/src/pages/PostForm/AskForm/AskForm.component.jsx +++ b/client/src/pages/PostForm/AskForm/AskForm.component.jsx @@ -2,6 +2,7 @@ import React, {Fragment, useState} from 'react'; import {connect} from 'react-redux'; import PropTypes from 'prop-types'; import {addPost} from '../../../redux/posts/posts.actions'; +import RichTexteditor from '../../../components/RichTextEditor/RichTextEditor.component'; import './AskForm.styles.scss'; @@ -59,7 +60,10 @@ const AskForm = ({addPost}) => { question

-