feat: add pictures extension to editor and canva

This commit is contained in:
swve 2022-12-13 18:18:51 +01:00
parent 643d4ae2e7
commit 5eb9101084
11 changed files with 243 additions and 8 deletions

View file

@ -4,6 +4,7 @@ import StarterKit from "@tiptap/starter-kit";
// Custom Extensions
import InfoCallout from "../Editor/Extensions/Callout/Info/InfoCallout";
import WarningCallout from "../Editor/Extensions/Callout/Warning/WarningCallout";
import ImageBlock from "../Editor/Extensions/Image/ImageBlock";
interface Editor {
content: string;
@ -24,6 +25,10 @@ function Canva(props: Editor) {
WarningCallout.configure({
editable: isEditable,
}),
ImageBlock.configure({
editable: isEditable,
element: props.element,
}),
],
content: props.content,

View file

@ -15,6 +15,7 @@ import Avvvatars from "avvvatars-react";
// extensions
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
import WarningCallout from "./Extensions/Callout/Warning/WarningCallout";
import ImageBlock from "./Extensions/Image/ImageBlock";
interface Editor {
content: string;
@ -30,6 +31,7 @@ function Editor(props: Editor) {
const editor: any = useEditor({
editable: true,
extensions: [
StarterKit.configure({
// The Collaboration extension comes with its own history handling
@ -41,6 +43,10 @@ function Editor(props: Editor) {
WarningCallout.configure({
editable: true,
}),
ImageBlock.configure({
editable: true,
element: props.element
}),
// Register the document with Tiptap
// Collaboration.configure({
// document: props.ydoc,
@ -244,7 +250,8 @@ const EditorContentWrapper = styled.div`
.ProseMirror {
padding-left: 20px;
padding-right: 20px;
padding-bottom: 6px;
padding-bottom: 20px;
padding-top: 1px;
&:focus {
outline: none !important;

View file

@ -7,7 +7,9 @@ export default Node.create({
name: "calloutInfo",
group: "block",
draggable: true,
content: "inline*",
content: "text*",
// TODO : multi line support
parseHTML() {
return [

View file

@ -7,7 +7,11 @@ export default Node.create({
name: "calloutWarning",
group: "block",
draggable: true,
content: "inline*",
content: "text*",
marks: "",
defining: true,
// TODO : multi line support
parseHTML() {
return [

View file

@ -0,0 +1,35 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import ImageBlockComponent from "./ImageBlockComponent";
export default Node.create({
name: "blockImage",
group: "block",
atom: true,
addAttributes() {
return {
fileObject: {
default: null,
},
};
},
parseHTML() {
return [
{
tag: "block-image",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["block-image", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return ReactNodeViewRenderer(ImageBlockComponent);
},
});

View file

@ -0,0 +1,89 @@
import { NodeViewWrapper } from "@tiptap/react";
import React from "react";
import styled from "styled-components";
import { AlertCircle, AlertTriangle, Image, ImagePlus, Info } from "lucide-react";
import { getImageFile, uploadNewImageFile } from "../../../../services/files/images";
import { getBackendUrl } from "../../../../services/config";
function ImageBlockComponent(props: any) {
const [image, setImage] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [fileObject, setfileObject] = React.useState(props.node.attrs.fileObject);
const handleImageChange = (event: React.ChangeEvent<any>) => {
setImage(event.target.files[0]);
};
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsLoading(true);
let object = await uploadNewImageFile(image, props.extension.options.element.element_id);
setIsLoading(false);
setfileObject(object);
props.updateAttributes({
fileObject: object,
});
};
return (
<NodeViewWrapper className="block-image">
{!fileObject && (
<BlockImageWrapper contentEditable={props.extension.options.editable}>
<div>
<Image color="#e1e0e0" size={50} />
<br />
</div>
<input onChange={handleImageChange} type="file" name="" id="" />
<br />
<button onClick={handleSubmit}>Submit</button>
</BlockImageWrapper>
)}
{fileObject && (
<BlockImage>
<img
src={`${getBackendUrl()}content/uploads/files/pictures/${props.extension.options.element.element_id}/${fileObject.file_id}.${
fileObject.file_format
}`}
alt=""
/>
</BlockImage>
)}
{isLoading && (
<div>
<AlertTriangle color="#e1e0e0" size={50} />
</div>
)}
</NodeViewWrapper>
);
}
export default ImageBlockComponent;
const BlockImageWrapper = styled.div`
display: flex;
flex-direction: column;
background: #f9f9f9;
border-radius: 3px;
padding: 30px;
min-height: 74px;
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
// center
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
const BlockImage = styled.div`
display: flex;
flex-direction: column;
img {
width: 100%;
border-radius: 6px;
height: 300px;
// cover
object-fit: cover;
}
`;
const ImageNotFound = styled.div``;

View file

@ -0,0 +1,29 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import VideoBlockComponent from "./VideoBlockComponent";
export default Node.create({
name: "calloutWarning",
group: "block",
draggable: true,
content: "inline*",
// TODO : multi line support
parseHTML() {
return [
{
tag: "callout-warning",
},
];
},
renderHTML({ HTMLAttributes }) {
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
},
addNodeView() {
return ReactNodeViewRenderer(VideoBlockComponent);
},
});

View file

@ -1,6 +1,6 @@
import styled from "styled-components";
import { FontBoldIcon, FontItalicIcon, StrikethroughIcon, ArrowLeftIcon, ArrowRightIcon, OpacityIcon } from "@radix-ui/react-icons";
import { AlertCircle, AlertTriangle, Info } from "lucide-react";
import { AlertCircle, AlertTriangle, ImagePlus, Info } from "lucide-react";
export const ToolbarButtons = ({ editor }: any) => {
if (!editor) {
@ -24,7 +24,15 @@ export const ToolbarButtons = ({ editor }: any) => {
<ToolBtn onClick={() => editor.chain().focus().toggleStrike().run()} className={editor.isActive("strike") ? "is-active" : ""}>
<StrikethroughIcon />
</ToolBtn>
<ToolSelect onChange={(e) => editor.chain().focus().toggleHeading({ level: parseInt(e.target.value) }).run() }>
<ToolSelect
onChange={(e) =>
editor
.chain()
.focus()
.toggleHeading({ level: parseInt(e.target.value) })
.run()
}
>
<option value="1">Heading 1</option>
<option value="2">Heading 2</option>
<option value="3">Heading 3</option>
@ -33,12 +41,25 @@ export const ToolbarButtons = ({ editor }: any) => {
<option value="6">Heading 6</option>
</ToolSelect>
{/* TODO: fix this : toggling only works one-way */}
<ToolBtn onClick={() => editor.chain().focus().toggleNode('calloutWarning').run()} >
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutWarning").run()}>
<AlertTriangle size={15} />
</ToolBtn>
<ToolBtn onClick={() => editor.chain().focus().toggleNode('calloutInfo').run()} >
<ToolBtn onClick={() => editor.chain().focus().toggleNode("calloutInfo").run()}>
<AlertCircle size={15} />
</ToolBtn>
<ToolBtn
onClick={() =>
editor
.chain()
.focus()
.insertContent({
type: "blockImage",
})
.run()
}
>
<ImagePlus size={15} />
</ToolBtn>
</ToolButtonsWrapper>
);
};
@ -60,7 +81,7 @@ const ToolBtn = styled.div`
margin-right: 5px;
transition: all 0.2s ease-in-out;
svg{
svg {
padding: 1px;
}

View file

@ -0,0 +1,38 @@
import { getAPIUrl } from "../config";
export async function uploadNewImageFile(file: any, element_id: string) {
const HeadersConfig = new Headers();
// Send file thumbnail as form data
const formData = new FormData();
formData.append("file_object", file);
formData.append("element_id", element_id);
const requestOptions: any = {
method: "POST",
headers: HeadersConfig,
redirect: "follow",
credentials: "include",
body: formData,
};
return fetch(`${getAPIUrl()}files/picture`, requestOptions)
.then((result) => result.json())
.catch((error) => console.log("error", error));
}
export async function getImageFile(file_id: string) {
const HeadersConfig = new Headers({ "Content-Type": "application/json" });
const requestOptions: any = {
method: "GET",
headers: HeadersConfig,
redirect: "follow",
credentials: "include",
};
// todo : add course id to url
return fetch(`${getAPIUrl()}files/picture?file_id=${file_id}`, requestOptions)
.then((result) => result.json())
.catch((error) => console.log("error", error));
}

View file

@ -3,6 +3,7 @@ from pydantic import BaseModel
from src.services.database import check_database, learnhouseDB, learnhouseDB
from fastapi import HTTPException, status, UploadFile
from fastapi.responses import StreamingResponse
import os
from src.services.users import PublicUser
@ -54,6 +55,10 @@ async def create_picture_file(picture_file: UploadFile, element_id: str):
element_id=element_id
)
# create folder for element
if not os.path.exists(f"content/uploads/files/pictures/{element_id}"):
os.mkdir(f"content/uploads/files/pictures/{element_id}")
# upload file to server
with open(f"content/uploads/files/pictures/{element_id}/{file_id}.{file_format}", 'wb') as f:
f.write(file)