From 4a1807e2480274455f439913f45f7df05fe87b82 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Thu, 22 Apr 2021 11:03:33 +0000 Subject: [PATCH] Resolve "Move tiny mce to settings panel" --- .../components/RndComponent.tsx | 89 +++++++++++++++++++ .../components/SlideEditor.tsx | 20 +---- .../components/SlideSettings.tsx | 31 ++++--- .../components/TextComponentDisplay.test.tsx | 18 ---- .../components/TextComponentDisplay.tsx | 80 ----------------- .../components/TextComponentEdit.tsx | 78 ++++++++++++++++ .../presentationEditor/components/styled.tsx | 22 ++++- client/src/pages/views/PresenterViewPage.tsx | 2 +- 8 files changed, 210 insertions(+), 130 deletions(-) create mode 100644 client/src/pages/presentationEditor/components/RndComponent.tsx delete mode 100644 client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx delete mode 100644 client/src/pages/presentationEditor/components/TextComponentDisplay.tsx create mode 100644 client/src/pages/presentationEditor/components/TextComponentEdit.tsx diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx new file mode 100644 index 00000000..b1b586ff --- /dev/null +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -0,0 +1,89 @@ +import axios from 'axios' +import React, { useState } from 'react' +import { Rnd } from 'react-rnd' +import { ComponentTypes } from '../../../enum/ComponentTypes' +import { useAppSelector } from '../../../hooks' +import { Component, ImageComponent, TextComponent } from '../../../interfaces/ApiModels' +import { Position, Size } from '../../../interfaces/Components' +import CheckboxComponent from './CheckboxComponent' +import ImageComponentDisplay from './ImageComponentDisplay' +import { TextComponentContainer } from './styled' + +type ImageComponentProps = { + component: Component +} + +const RndComponent = ({ component }: ImageComponentProps) => { + const [hover, setHover] = useState(false) + const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) + const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) + const competitionId = useAppSelector((state) => state.editor.competition.id) + const slideId = useAppSelector((state) => state.editor.activeSlideId) + const handleUpdatePos = (pos: Position) => { + // TODO: change path to /slides/${slideId} + axios.put(`/competitions/${competitionId}/slides/0/components/${component.id}`, { + x: pos.x, + y: pos.y, + }) + } + const handleUpdateSize = (size: Size) => { + // TODO: change path to /slides/${slideId} + axios.put(`/competitions/${competitionId}/slides/0/components/${component.id}`, { + w: size.w, + h: size.h, + }) + } + + const renderInnerComponent = () => { + switch (component.type_id) { + case ComponentTypes.Checkbox: + return <CheckboxComponent key={component.id} component={component} /> + case ComponentTypes.Text: + return ( + <TextComponentContainer + hover={hover} + dangerouslySetInnerHTML={{ __html: (component as TextComponent).data.text }} + /> + ) + case ComponentTypes.Image: + return <ImageComponentDisplay key={component.id} component={component as ImageComponent} /> + default: + break + } + } + + return ( + <Rnd + minWidth={50} + minHeight={50} + bounds="parent" + onDragStop={(e, d) => { + setCurrentPos({ x: d.x, y: d.y }) + handleUpdatePos(d) + }} + onMouseEnter={() => setHover(true)} + onMouseLeave={() => setHover(false)} + size={{ width: currentSize.w, height: currentSize.h }} + position={{ x: currentPos.x, y: currentPos.y }} + onResizeStop={(e, direction, ref, delta, position) => { + setCurrentSize({ + w: ref.offsetWidth, + h: ref.offsetHeight, + }) + setCurrentPos(position) + handleUpdateSize({ w: ref.offsetWidth, h: ref.offsetHeight }) + handleUpdatePos(position) + }} + onResize={(e, direction, ref, delta, position) => + setCurrentSize({ + w: ref.offsetWidth, + h: ref.offsetHeight, + }) + } + > + {renderInnerComponent()} + </Rnd> + ) +} + +export default RndComponent diff --git a/client/src/pages/presentationEditor/components/SlideEditor.tsx b/client/src/pages/presentationEditor/components/SlideEditor.tsx index a71b9262..457f903f 100644 --- a/client/src/pages/presentationEditor/components/SlideEditor.tsx +++ b/client/src/pages/presentationEditor/components/SlideEditor.tsx @@ -1,11 +1,7 @@ import React from 'react' -import { ComponentTypes } from '../../../enum/ComponentTypes' import { useAppSelector } from '../../../hooks' -import { ImageComponent, TextComponent } from '../../../interfaces/ApiModels' -import CheckboxComponent from './CheckboxComponent' -import ImageComponentDisplay from './ImageComponentDisplay' +import RndComponent from './RndComponent' import { SlideEditorContainer, SlideEditorContainerRatio, SlideEditorPaper } from './styled' -import TextComponentDisplay from './TextComponentDisplay' const SlideEditor: React.FC = () => { const components = useAppSelector( @@ -16,19 +12,7 @@ const SlideEditor: React.FC = () => { <SlideEditorContainer> <SlideEditorContainerRatio> <SlideEditorPaper> - {components && - components.map((component) => { - switch (component.type_id) { - case ComponentTypes.Checkbox: - return <CheckboxComponent key={component.id} component={component} /> - case ComponentTypes.Text: - return <TextComponentDisplay key={component.id} component={component as TextComponent} /> - case ComponentTypes.Image: - return <ImageComponentDisplay key={component.id} component={component as ImageComponent} /> - default: - break - } - })} + {components && components.map((component) => <RndComponent key={component.id} component={component} />)} </SlideEditorPaper> </SlideEditorContainerRatio> </SlideEditorContainer> diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index e59586bb..e6be5c15 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -6,6 +6,7 @@ import { DialogContent, DialogContentText, DialogTitle, + Divider, FormControl, InputLabel, List, @@ -26,7 +27,8 @@ import { useParams } from 'react-router-dom' import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' import { QuestionAlternative, TextComponent } from '../../../interfaces/ApiModels' -import { HiddenInput } from './styled' +import { HiddenInput, TextCard } from './styled' +import TextComponentEdit from './TextComponentEdit' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -234,9 +236,15 @@ const SlideSettings: React.FC = () => { } const handleAddText = async () => { - console.log('Add text component') - // TODO: post the new text] - // setTexts([...texts, { id: 'newText', name: 'New Text' }]) + if (activeSlide) { + await axios.post(`/competitions/${id}/slides/${activeSlide?.order}/components`, { + type_id: 1, + data: { text: 'Ny text' }, + w: 315, + h: 50, + }) + dispatch(getEditorCompetition(id)) + } } const GreenCheckbox = withStyles({ @@ -338,9 +346,8 @@ const SlideSettings: React.FC = () => { helperText="Lämna blank för att inte använda timerfunktionen" label="Timer" type="number" - defaultValue={activeSlide?.timer || 0} onChange={updateTimer} - value={timer} + value={timer || ''} /> </ListItem> @@ -383,13 +390,13 @@ const SlideSettings: React.FC = () => { </ListItem> {texts && texts.map((text) => ( - <div key={text.id}> - <ListItem divider> - <TextField className={classes.textInput} label={text.data.text} variant="outlined" /> - <CloseIcon className={classes.clickableIcon} /> - </ListItem> - </div> + <TextCard elevation={4} key={text.id}> + <TextComponentEdit component={text} /> + + <Divider /> + </TextCard> ))} + <ListItem className={classes.center} button onClick={handleAddText}> <Typography className={classes.addButtons} variant="button"> Lägg till text diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx deleted file mode 100644 index c4489878..00000000 --- a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Editor } from '@tinymce/tinymce-react' -import { mount } from 'enzyme' -import React from 'react' -import { Provider } from 'react-redux' -import store from '../../../store' -import TextComponentDisplay from './TextComponentDisplay' - -it('renders text component display', () => { - const testText = 'TEST' - const container = mount( - <Provider store={store}> - <TextComponentDisplay - component={{ id: 0, x: 0, y: 0, w: 0, h: 0, data: { text: testText, font: '123123' }, type_id: 2 }} - /> - </Provider> - ) - expect(container.find(Editor).prop('initialValue')).toBe(testText) -}) diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx deleted file mode 100644 index 4fb34650..00000000 --- a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Editor } from '@tinymce/tinymce-react' -import axios from 'axios' -import React, { useState } from 'react' -import { Rnd } from 'react-rnd' -import { useAppSelector } from '../../../hooks' -import { TextComponent } from '../../../interfaces/ApiModels' -import { Position, Size } from '../../../interfaces/Components' - -type ImageComponentProps = { - component: TextComponent -} - -const TextComponentDisplay = ({ component }: ImageComponentProps) => { - const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) - const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) - const competitionId = useAppSelector((state) => state.editor.competition.id) - const slideId = useAppSelector((state) => state.editor.activeSlideId) - if (component.id === 1) console.log(component) - const handleEditorChange = (e: any) => { - console.log('Content was updated:', e.target.getContent()) - axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { - data: { ...component.data, text: e.target.getContent() }, - }) - } - const handleUpdatePos = (pos: Position) => { - axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { - x: pos.x, - y: pos.y, - }) - } - const handleUpdateSize = () => { - axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { - w: currentSize.w, - h: currentSize.h, - }) - } - return ( - <Rnd - minWidth={50} - minHeight={50} - bounds="parent" - onDragStop={(e, d) => { - setCurrentPos({ x: d.x, y: d.y }) - handleUpdatePos(d) - }} - size={{ width: currentSize.w, height: currentSize.h }} - position={{ x: currentPos.x, y: currentPos.y }} - onResize={(e, direction, ref, delta, position) => { - setCurrentSize({ - w: ref.offsetWidth, - h: ref.offsetHeight, - }) - setCurrentPos(position) - }} - onResizeStop={handleUpdateSize} - > - <div style={{ height: '100%', width: '100%' }}> - <Editor - initialValue={component.data.text} - init={{ - height: '100%', - menubar: false, - plugins: [ - 'advlist autolink lists link image charmap print preview anchor', - 'searchreplace visualblocks code fullscreen', - 'insertdatetime media table paste code help wordcount', - ], - toolbar: - 'undo redo | formatselect | fontselect | bold italic backcolor | \ - alignleft aligncenter alignright alignjustify | \ - bullist numlist outdent indent | removeformat | help', - }} - onChange={handleEditorChange} - /> - </div> - </Rnd> - ) -} - -export default TextComponentDisplay diff --git a/client/src/pages/presentationEditor/components/TextComponentEdit.tsx b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx new file mode 100644 index 00000000..92202993 --- /dev/null +++ b/client/src/pages/presentationEditor/components/TextComponentEdit.tsx @@ -0,0 +1,78 @@ +import { Editor } from '@tinymce/tinymce-react' +import axios from 'axios' +import React, { useEffect, useState } from 'react' +import { useParams } from 'react-router-dom' +import { getEditorCompetition } from '../../../actions/editor' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { TextComponent } from '../../../interfaces/ApiModels' +import { DeleteTextButton } from './styled' + +type ImageComponentProps = { + component: TextComponent +} + +interface CompetitionParams { + id: string +} + +const TextComponentEdit = ({ component }: ImageComponentProps) => { + const { id }: CompetitionParams = useParams() + const competitionId = useAppSelector((state) => state.editor.competition.id) + const [content, setContent] = useState('') + const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined) + const dispatch = useAppDispatch() + + useEffect(() => { + setContent(component.data.text) + }, []) + + const handleSaveText = async (a: string) => { + setContent(a) + if (timerHandle) { + clearTimeout(timerHandle) + setTimerHandle(undefined) + } + //Only updates 250ms after last input was made to not spam + setTimerHandle( + window.setTimeout(async () => { + console.log('Content was updated on server. id: ', component.id) + await axios.put(`/competitions/${competitionId}/slides/0/components/${component.id}`, { + data: { ...component.data, text: a }, + }) + dispatch(getEditorCompetition(id)) + }, 250) + ) + } + + const handleDeleteText = async (componentId: number) => { + await axios.delete(`/competitions/${id}/slides/0/components/${componentId}`) + dispatch(getEditorCompetition(id)) + } + + return ( + <div style={{ minHeight: '300px', height: '100%', width: '100%' }}> + <Editor + value={content || ''} + init={{ + height: '300px', + menubar: false, + plugins: [ + 'advlist autolink lists link image charmap print preview anchor', + 'searchreplace visualblocks code fullscreen', + 'insertdatetime media table paste code help wordcount', + ], + toolbar: + 'undo redo save | fontselect | formatselect | bold italic backcolor | \ + alignleft aligncenter alignright alignjustify | \ + bullist numlist outdent indent | removeformat | help', + }} + onEditorChange={(a, e) => handleSaveText(a)} + /> + <DeleteTextButton variant="contained" color="secondary" onClick={() => handleDeleteText(component.id)}> + Ta bort + </DeleteTextButton> + </div> + ) +} + +export default TextComponentEdit diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 32df0ac9..8bee7f0d 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -1,4 +1,4 @@ -import { Tab } from '@material-ui/core' +import { Button, Card, Tab } from '@material-ui/core' import styled from 'styled-components' export const SettingsTab = styled(Tab)` @@ -44,3 +44,23 @@ export const ToolbarPadding = styled.div` height: 0; padding-top: 55px; ` + +export const TextCard = styled(Card)` + margin-bottom: 15px; + margin-top: 10px; +` + +export const DeleteTextButton = styled(Button)` + width: 100%; + margin-bottom: 7px; +` + +interface TextComponentContainerProps { + hover: boolean +} + +export const TextComponentContainer = styled.div<TextComponentContainerProps>` + height: 100%; + width: 100%; + border: solid ${(props) => (props.hover ? 1 : 0)}px; +` diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx index 3bb20a43..40aa252d 100644 --- a/client/src/pages/views/PresenterViewPage.tsx +++ b/client/src/pages/views/PresenterViewPage.tsx @@ -67,7 +67,7 @@ const PresenterViewPage: React.FC = () => { socket_connect() socketSetSlide // Behövs denna? setTimeout(startCompetition, 500) // Ghetto, wait for everything to load - console.log(id) + // console.log(id) }, []) const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => { -- GitLab