From 2757cc969ec497d8ce0ceb6e0d2208d2c5f6b67c Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Mon, 12 Apr 2021 15:31:51 +0000 Subject: [PATCH] Implement drag and resize --- client/package-lock.json | 58 +++++++++++++++++ client/package.json | 2 + client/src/enum/ComponentTypes.ts | 5 ++ client/src/index.css | 9 +++ client/src/interfaces/Components.ts | 9 +++ .../PresentationEditorPage.tsx | 15 +++-- .../components/CheckboxComponent.tsx | 31 +++++++++ .../components/CompetitionSettings.tsx | 4 +- .../components/ImageComponentDisplay.tsx | 44 +++++++++++++ .../components/SlideEditor.tsx | 39 ++++++++++++ .../components/SlideSettings.tsx | 4 +- .../components/TextComponentDisplay.tsx | 63 +++++++++++++++++++ .../presentationEditor/components/styled.tsx | 8 +++ .../src/pages/presentationEditor/styled.tsx | 4 ++ client/src/pages/views/styled.tsx | 1 + 15 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 client/src/enum/ComponentTypes.ts create mode 100644 client/src/interfaces/Components.ts create mode 100644 client/src/pages/presentationEditor/components/CheckboxComponent.tsx create mode 100644 client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx create mode 100644 client/src/pages/presentationEditor/components/SlideEditor.tsx create mode 100644 client/src/pages/presentationEditor/components/TextComponentDisplay.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 133d1ca2..b08413fa 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2262,6 +2262,15 @@ "@babel/runtime": "^7.12.5" } }, + "@tinymce/tinymce-react": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-3.12.2.tgz", + "integrity": "sha512-M6YQ9e+9rpxrZDOeNPmjgroEfooEKiMVuI4I3+xQtMX1hQQ/t9pGE9nUdS0faZDvqhlc8B/w12GJQNU5ekPo4g==", + "requires": { + "prop-types": "^15.6.2", + "tinymce": "^5.7.1" + } + }, "@types/anymatch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", @@ -4635,6 +4644,11 @@ } } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -7211,6 +7225,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-memoize": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", + "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==" + }, "fastq": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz", @@ -13382,6 +13401,14 @@ } } }, + "re-resizable": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.0.tgz", + "integrity": "sha512-3cUDG81ylyqI0Pdgle/RHwwRYq0ORZzsUaySOCO8IbEtNyaRtrIHYm/jMQ5pjcNiKCxR3vsSymIQZHwJq4gg2Q==", + "requires": { + "fast-memoize": "^2.5.1" + } + }, "react": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", @@ -13525,6 +13552,15 @@ "scheduler": "^0.20.1" } }, + "react-draggable": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.3.tgz", + "integrity": "sha512-jV4TE59MBuWm7gb6Ns3Q1mxX8Azffb7oTtDtBgFkxRvhDp38YAARmRplrj0+XGkhOJB5XziArX+4HUUABtyZ0w==", + "requires": { + "classnames": "^2.2.5", + "prop-types": "^15.6.0" + } + }, "react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", @@ -13557,6 +13593,23 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", "integrity": "sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==" }, + "react-rnd": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.2.4.tgz", + "integrity": "sha512-wseACIsxa1wuZz9XatO3/JAZR748Sddehh0NtJz1Yj3X5BQm5pwRShiadfnWrUajJATurHbN0NVTUn+jEkHkPw==", + "requires": { + "re-resizable": "6.9.0", + "react-draggable": "4.4.3", + "tslib": "2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + } + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", @@ -15912,6 +15965,11 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "tinymce": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-5.7.1.tgz", + "integrity": "sha512-1gY8RClc734srSlkYwY0MQzmkS1j73PuPC+nYtNtrrQVPY9VNcZ4bOiRwzTbdjPPD8GOtv6BAk8Ww/H2RiqKpA==" + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", diff --git a/client/package.json b/client/package.json index 6e4d3573..f41350a6 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "@testing-library/jest-dom": "^5.11.9", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.6.3", + "@tinymce/tinymce-react": "^3.12.2", "@types/jest": "^26.0.20", "@types/node": "^12.19.16", "@types/react": "^17.0.1", @@ -21,6 +22,7 @@ "react-axios": "^2.0.4", "react-dom": "^17.0.1", "react-redux": "^7.2.2", + "react-rnd": "^10.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.2", "redux": "^4.0.5", diff --git a/client/src/enum/ComponentTypes.ts b/client/src/enum/ComponentTypes.ts new file mode 100644 index 00000000..c8d194c0 --- /dev/null +++ b/client/src/enum/ComponentTypes.ts @@ -0,0 +1,5 @@ +export enum ComponentTypes { + Text, + Checkbox, + Image, +} diff --git a/client/src/index.css b/client/src/index.css index 80bfba85..6b920fab 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -7,6 +7,15 @@ body { -moz-osx-font-smoothing: grayscale; } +.tox-edit-area__iframe, +.tox-edit-area { + background: transparent !important; +} + +.tox-notifications-container { + display: none; +} + html, #root { height: 100%; diff --git a/client/src/interfaces/Components.ts b/client/src/interfaces/Components.ts new file mode 100644 index 00000000..864b987d --- /dev/null +++ b/client/src/interfaces/Components.ts @@ -0,0 +1,9 @@ +export interface Position { + x: number + y: number +} + +export interface Size { + w: number + h: number +} diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 5a0dbc99..a5209a16 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -7,8 +7,10 @@ import ListItemText from '@material-ui/core/ListItemText' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import React from 'react' import { useParams } from 'react-router-dom' +import { Content } from '../views/styled' import SettingsPanel from './components/SettingsPanel' -import { SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' +import SlideEditor from './components/SlideEditor' +import { PresentationEditorContainer, SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' function createSlide(name: string) { return { name } @@ -28,9 +30,6 @@ const rightDrawerWidth = 390 const useStyles = makeStyles((theme: Theme) => createStyles({ - root: { - display: 'flex', - }, appBar: { width: `calc(100% - ${rightDrawerWidth}px)`, marginLeft: leftDrawerWidth, @@ -71,7 +70,7 @@ const PresentationEditorPage: React.FC = () => { const classes = useStyles() const params: CompetitionParams = useParams() return ( - <div className={classes.root}> + <PresentationEditorContainer> <CssBaseline /> <AppBar position="fixed" className={classes.appBar}> <ToolBarContainer> @@ -120,7 +119,11 @@ const PresentationEditorPage: React.FC = () => { > <SettingsPanel></SettingsPanel> </Drawer> - </div> + + <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> + <SlideEditor /> + </Content> + </PresentationEditorContainer> ) } diff --git a/client/src/pages/presentationEditor/components/CheckboxComponent.tsx b/client/src/pages/presentationEditor/components/CheckboxComponent.tsx new file mode 100644 index 00000000..cb264712 --- /dev/null +++ b/client/src/pages/presentationEditor/components/CheckboxComponent.tsx @@ -0,0 +1,31 @@ +import { Checkbox } from '@material-ui/core' +import React, { useState } from 'react' +import { Rnd } from 'react-rnd' +import { Component } from '../../../interfaces/ApiModels' +import { Position } from '../../../interfaces/Components' + +type CheckboxComponentProps = { + component: Component +} + +const CheckboxComponent = ({ component }: CheckboxComponentProps) => { + const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) + return ( + <Rnd + bounds="parent" + onDragStop={(e, d) => { + setCurrentPos({ x: d.x, y: d.y }) + }} + position={{ x: currentPos.x, y: currentPos.y }} + > + <Checkbox + disableRipple + style={{ + transform: 'scale(3)', + }} + /> + </Rnd> + ) +} + +export default CheckboxComponent diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index a9a1d767..7446c6d1 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -48,9 +48,9 @@ const CompetitionSettings: React.FC = () => { return ( <div className={classes.textInputContainer}> <form noValidate autoComplete="off"> - <TextField className={classes.textInput} id="outlined-basic" label="Tävlingsnamn" variant="outlined" /> + <TextField className={classes.textInput} label="Tävlingsnamn" variant="outlined" /> <Divider /> - <TextField className={classes.textInput} id="outlined-basic" label="Stad" variant="outlined" /> + <TextField className={classes.textInput} label="Stad" variant="outlined" /> </form> <List> diff --git a/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx new file mode 100644 index 00000000..cffba9e5 --- /dev/null +++ b/client/src/pages/presentationEditor/components/ImageComponentDisplay.tsx @@ -0,0 +1,44 @@ +import React, { useState } from 'react' +import { Rnd } from 'react-rnd' +import { ImageComponent } from '../../../interfaces/ApiModels' +import { Position, Size } from '../../../interfaces/Components' + +type ImageComponentProps = { + component: ImageComponent +} + +const ImageComponentDisplay = ({ component }: ImageComponentProps) => { + const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) + const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) + return ( + <Rnd + minWidth={50} + minHeight={50} + bounds="parent" + onDragStop={(e, d) => { + setCurrentPos({ x: d.x, y: d.y }) + }} + 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={() => { + console.log('Skicka data till server') + }} + > + <img + src="https://365psd.com/images/previews/c61/cartoon-cow-52394.png" + height={currentSize.h} + width={currentSize.w} + draggable={false} + /> + </Rnd> + ) +} + +export default ImageComponentDisplay diff --git a/client/src/pages/presentationEditor/components/SlideEditor.tsx b/client/src/pages/presentationEditor/components/SlideEditor.tsx new file mode 100644 index 00000000..22a73ef3 --- /dev/null +++ b/client/src/pages/presentationEditor/components/SlideEditor.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { ComponentTypes } from '../../../enum/ComponentTypes' +import CheckboxComponent from './CheckboxComponent' +import ImageComponentDisplay from './ImageComponentDisplay' +import { SlideEditorContainer } from './styled' +import TextComponentDisplay from './TextComponentDisplay' + +const SlideEditor: React.FC = () => { + // const components = useAppSelector(state => state.editor.slide.components) // get the current RichSlide + const components: any[] = [ + { id: 0, x: 15, y: 150, w: 200, h: 300, type: ComponentTypes.Checkbox }, + { id: 1, x: 15, y: 250, w: 200, h: 300, type: ComponentTypes.Checkbox }, + { id: 2, x: 15, y: 350, w: 200, h: 300, type: ComponentTypes.Checkbox }, + { id: 3, x: 300, y: 500, w: 100, h: 300, type: ComponentTypes.Text, text: 'text component', font: 'arial' }, + { id: 4, x: 250, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, + { id: 5, x: 350, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, + ] + return ( + <SlideEditorContainer> + {components.map((component) => { + switch (component.type) { + case ComponentTypes.Checkbox: + return <CheckboxComponent key={component.id} component={component} /> + break + case ComponentTypes.Text: + return <TextComponentDisplay key={component.id} component={component} /> + break + case ComponentTypes.Image: + return <ImageComponentDisplay key={component.id} component={component} /> + break + default: + break + } + })} + </SlideEditorContainer> + ) +} + +export default SlideEditor diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index 936f3d8b..4b094021 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -137,7 +137,7 @@ const SlideSettings: React.FC = () => { {answers.map((answer) => ( <div key={answer.id}> <ListItem divider> - <TextField className={classes.textInput} id="outlined-basic" label={answer.name} variant="outlined" /> + <TextField className={classes.textInput} label={answer.name} variant="outlined" /> <Checkbox color="default" /> <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(answer.id)} /> </ListItem> @@ -155,7 +155,7 @@ const SlideSettings: React.FC = () => { {texts.map((text) => ( <div key={text.id}> <ListItem divider> - <TextField className={classes.textInput} id="outlined-basic" label={text.name} variant="outlined" /> + <TextField className={classes.textInput} label={text.name} variant="outlined" /> <MoreHorizOutlinedIcon className={classes.clickableIcon} /> <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseTextClick(text.id)} /> </ListItem> diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx new file mode 100644 index 00000000..30426b9d --- /dev/null +++ b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx @@ -0,0 +1,63 @@ +import { Editor } from '@tinymce/tinymce-react' +import React, { useState } from 'react' +import { Rnd } from 'react-rnd' +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 handleEditorChange = (e: any) => { + console.log('Content was updated:', e.target.getContent()) + //TODO: axios.post + } + return ( + <Rnd + minWidth={50} + minHeight={50} + bounds="parent" + onDragStop={(e, d) => { + setCurrentPos({ x: d.x, y: d.y }) + }} + 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={() => { + console.log('skickar till server') + }} + > + <div style={{ height: '100%', width: '100%' }}> + <Editor + initialValue={component.text} + init={{ + body_class: 'mceBlackBody', + 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/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 81459a54..62239c5b 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -5,3 +5,11 @@ export const SettingsTab = styled(Tab)` height: 64px; min-width: 195px; ` + +export const SlideEditorContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +` diff --git a/client/src/pages/presentationEditor/styled.tsx b/client/src/pages/presentationEditor/styled.tsx index 6ba71760..462acf2e 100644 --- a/client/src/pages/presentationEditor/styled.tsx +++ b/client/src/pages/presentationEditor/styled.tsx @@ -19,3 +19,7 @@ export const SlideListItem = styled(ListItem)` text-align: center; height: 60px; ` + +export const PresentationEditorContainer = styled.div` + height: 100%; +` diff --git a/client/src/pages/views/styled.tsx b/client/src/pages/views/styled.tsx index 261727b9..d4a9cf9e 100644 --- a/client/src/pages/views/styled.tsx +++ b/client/src/pages/views/styled.tsx @@ -87,4 +87,5 @@ export const Content = styled.div<ContentProps>` margin-left: ${(props) => (props ? props.leftDrawerWidth : 0)}px; margin-right: ${(props) => (props ? props.rightDrawerWidth : 0)}px; width: calc(100% - ${(props) => (props ? props.leftDrawerWidth + props.rightDrawerWidth : 0)}px); + height: calc(100% - 64px); ` -- GitLab