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
-