Skip to content

Commit

Permalink
Merge pull request #42 from ANovaBerkeley/rich-text-editor
Browse files Browse the repository at this point in the history
Rich text editor
  • Loading branch information
cidneyweng authored Mar 28, 2021
2 parents e210993 + 45ea690 commit aacf6cc
Show file tree
Hide file tree
Showing 10 changed files with 422 additions and 89 deletions.
169 changes: 106 additions & 63 deletions client/src/components/LessonPage.js
Original file line number Diff line number Diff line change
@@ -1,116 +1,159 @@
import React, { useState, useEffect } from 'react';
import { Modal } from 'antd';
import { FiEdit } from 'react-icons/fi';
import ContentEditable from 'react-contenteditable';

import { EditorState, convertToRaw, convertFromRaw } from 'draft-js';
import TextEditor from './TextEditor';
import 'draft-js/dist/Draft.css';
import '../stylesheets/LessonPage.css';
import { handleErrors } from '../utils/helpers';

const LessonPage = props => {
const { id, ismentor } = props;

const [editMode, setEditMode] = useState(false);
const [title, setTitle] = useState('');
const [descriptionHTML, setDescriptionHTML] = useState('');
const [resourcesHTML, setResourcesHTML] = useState('');
const [labHTML, setLabHTML] = useState('');
const [exitTicketHTML, setExitTicketHTML] = useState('');
const [descriptionState, setDescriptionState] = useState(EditorState.createEmpty());
const [resourcesState, setResourcesState] = useState(EditorState.createEmpty());
const [labState, setLabState] = useState(EditorState.createEmpty());
const [exitTicketState, setExitTicketState] = useState(EditorState.createEmpty());
const [oldDescriptionState, setOldDescriptionState] = useState(
EditorState.createEmpty(),
);
const [oldResourcesState, setOldResourcesState] = useState(EditorState.createEmpty());
const [oldLabState, setOldLabState] = useState(EditorState.createEmpty());
const [oldExitTicketState, setOldExitTicketState] = useState(EditorState.createEmpty());

useEffect(() => {
fetch('/api/v1/lessons/' + id + '?id=' + id)
.then(res => res.json())
.then(lesson => {
setTitle(lesson[0].title);
setDescriptionHTML(lesson[0].descriptionHTML);
setResourcesHTML(lesson[0].resourcesHTML);
setLabHTML(lesson[0].labHTML);
setExitTicketHTML(lesson[0].exitTicketHTML);
if (lesson[0].description_state) {
const content = convertFromRaw(lesson[0].description_state);
setDescriptionState(EditorState.createWithContent(content));
setOldDescriptionState(EditorState.createWithContent(content));
}
if (lesson[0].resources_state) {
const content = convertFromRaw(lesson[0].resources_state);
setResourcesState(EditorState.createWithContent(content));
setOldResourcesState(EditorState.createWithContent(content));
}
if (lesson[0].lab_state) {
const content = convertFromRaw(lesson[0].lab_state);
setLabState(EditorState.createWithContent(content));
setOldLabState(EditorState.createWithContent(content));
}
if (lesson[0].exit_ticket_state) {
const content = convertFromRaw(lesson[0].exit_ticket_state);
setExitTicketState(EditorState.createWithContent(content));
setOldExitTicketState(EditorState.createWithContent(content));
}
});
}, [id]);

const handleDescriptionChange = evt => {
setDescriptionHTML(evt.target.value);
const handleDescriptionChange = newState => {
setDescriptionState(newState);
console.log(newState);
};

const handleResourcesChange = newState => {
setResourcesState(newState);
};

const handleResourcesChange = evt => {
setResourcesHTML(evt.target.value);
const handleLabChange = newState => {
setLabState(newState);
};

const handleLabChange = evt => {
setLabHTML(evt.target.value);
const handleExitTicketChange = newState => {
setExitTicketState(newState);
};

const handleExitTicketChange = evt => {
setExitTicketHTML(evt.target.value);
const cancelChanges = () => {
setDescriptionState(oldDescriptionState);
setResourcesState(oldResourcesState);
setLabState(oldLabState);
setExitTicketState(oldExitTicketState);
setEditMode(false);
};

const saveChanges = () => {
fetch('/api/v1/lessons/updatePage', {
method: 'POST',
body: JSON.stringify({
lessonId: id,
editedDescriptionHTML: descriptionHTML,
editedResourcesHTML: resourcesHTML,
editedLabHTML: labHTML,
editedExitTicketHTML: exitTicketHTML,
editedDescriptionState: convertToRaw(descriptionState.getCurrentContent()),
editedResourcesState: convertToRaw(resourcesState.getCurrentContent()),
editedLabState: convertToRaw(labState.getCurrentContent()),
editedExitTicketState: convertToRaw(exitTicketState.getCurrentContent()),
}),
headers: new Headers({
'Content-Type': 'application/json',
}),
}).then(setEditMode(false));
})
.then(handleErrors)
.then(() => {
setOldDescriptionState(descriptionState);
setOldResourcesState(resourcesState);
setOldLabState(labState);
setOldExitTicketState(exitTicketState);
setEditMode(false);
})
.catch(() =>
Modal.error({
title: 'Unable to save changes.',
centered: true,
}),
);
};

let maybeEditButton;
if (ismentor) {
maybeEditButton = (
<button className="editButton" onClick={() => setEditMode(true)} type="button">
<FiEdit size="42" />
</button>
);
}
let maybeSaveButton;
let editing = '';
if (editMode) {
maybeSaveButton = (
<button className="saveButton" onClick={saveChanges} type="button">
Save
</button>
);
editing = 'editing';
}
return (
<div className="page">
<div className="lessonPageContainer">
<div className="title-container">
<h1 className="lessonPageTitle">{title}</h1>
{maybeEditButton}
{ismentor && (
<button
className="editButton"
onClick={() => setEditMode(true)}
type="button"
>
<FiEdit size="42" />
</button>
)}
</div>
<ContentEditable
className={'textBox ' + editing}
html={descriptionHTML}
disabled={!editMode}
onChange={handleDescriptionChange} // handle innerHTML change
<TextEditor
editorState={descriptionState}
editMode={editMode}
onChange={handleDescriptionChange}
/>
<h2 className="textTitle"> Lesson Resources </h2>
<ContentEditable
className={'textBox ' + editing}
html={resourcesHTML}
disabled={!editMode}
onChange={handleResourcesChange} // handle innerHTML change
<TextEditor
editorState={resourcesState}
editMode={editMode}
onChange={handleResourcesChange}
/>
<h2 className="textTitle"> Lab </h2>
<ContentEditable
className={'textBox ' + editing}
html={labHTML}
disabled={!editMode}
onChange={handleLabChange} // handle innerHTML change
<TextEditor
editorState={labState}
editMode={editMode}
onChange={handleLabChange}
/>
<h2 className="textTitle"> Exit Ticket </h2>
<ContentEditable
className={'textBox ' + editing}
html={exitTicketHTML}
disabled={!editMode}
onChange={handleExitTicketChange} // handle innerHTML change
<TextEditor
editorState={exitTicketState}
editMode={editMode}
onChange={handleExitTicketChange}
/>
{maybeSaveButton}
{editMode && (
<div className="buttonsContainer">
<button className="cancelButton" onClick={cancelChanges} type="button">
Cancel
</button>
<button className="saveButton" onClick={saveChanges} type="button">
Save
</button>
</div>
)}
</div>
</div>
);
Expand Down
1 change: 0 additions & 1 deletion client/src/components/SiteLessons.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useState, useEffect } from 'react';
// import { useAsync } from "react-async"
import { Modal, DatePicker, Row, Input } from 'antd';
import * as decode from 'jwt-decode';
import { GoPlus } from 'react-icons/go';
Expand Down
86 changes: 86 additions & 0 deletions client/src/components/TextEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { RichUtils, EditorState } from 'draft-js';
import 'draft-js/dist/Draft.css';
import Editor from 'draft-js-plugins-editor';
import addLinkPlugin from '../utils/addLink.js';

const TextEditor = props => {
const { editMode, editorState, onChange } = props;

const editing = editMode ? 'editing' : '';

const plugins = [addLinkPlugin];

const handleKeyCommand = command => {
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
onChange(newState);
}
};

const onUnderlineClick = () => {
onChange(RichUtils.toggleInlineStyle(editorState, 'UNDERLINE'));
};

const onBoldClick = () => {
onChange(RichUtils.toggleInlineStyle(editorState, 'BOLD'));
};

const onItalicClick = () => {
onChange(RichUtils.toggleInlineStyle(editorState, 'ITALIC'));
};

const onAddLinkClick = () => {
const selection = editorState.getSelection();
const link = window.prompt('Embed hyperlink here : ', 'https://');
if (!link) {
onChange(RichUtils.toggleLink(editorState, selection, null));
return 'handled';
} else if (!link.includes('http')) {
onChange(RichUtils.toggleLink(editorState, selection, null));
window.alert('Error: Must include "https://" in link');
return 'handled';
}
const content = editorState.getCurrentContent();
const contentWithEntity = content.createEntity('LINK', 'MUTABLE', { url: link });
const newEditorState = EditorState.push(
editorState,
contentWithEntity,
'create-entity',
);
const entityKey = contentWithEntity.getLastCreatedEntityKey();
onChange(RichUtils.toggleLink(newEditorState, selection, entityKey));
};

return (
<div className={'textBox ' + editing}>
{editMode && (
<>
<div>
<button className="editorButton" onClick={onUnderlineClick}>
U
</button>
<button className="editorButton" onClick={onBoldClick}>
<b>B</b>
</button>
<button className="editorButton" onClick={onItalicClick}>
<em>I</em>
</button>
<button className="editorButton" onClick={onAddLinkClick}>
Link
</button>
</div>
</>
)}
<Editor
editorState={editorState}
handleKeyCommand={handleKeyCommand}
onChange={onChange}
plugins={plugins}
readOnly={!editMode}
/>
</div>
);
};

export default TextEditor;
Loading

0 comments on commit aacf6cc

Please sign in to comment.