Skip to content

Commit

Permalink
Merge pull request #25 from uhh-lt/color-picker
Browse files Browse the repository at this point in the history
Added color picker
  • Loading branch information
bigabig authored Aug 30, 2022
2 parents a732c57 + 07c1746 commit a7b0680
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 32 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"i18next": "^21.8.9",
"lodash": "^4.17.21",
"react": "^18.1.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.1.0",
"react-hook-form": "^7.32.0",
"react-i18next": "^11.17.1",
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/utils/ColorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// see https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-r
function rgbStringToHex(rgb: string) {
let result = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/i.exec(rgb);
return result ? rgbToHex(parseInt(result[1]), parseInt(result[2]), parseInt(result[3])) : null;
}

function rgbToHex(r: number, g: number, b: number) {
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}

function hexToRgb(hex: string) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function (m, r, g, b) {
return r + r + g + g + b + b;
});

let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
}

const ColorUtils = {
rgbStringToHex,
rgbToHex,
hexToRgb,
};

export default ColorUtils;
54 changes: 43 additions & 11 deletions frontend/src/views/annotation/CodeExplorer/CodeCreationDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField } from "@mui/material";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
Stack,
TextField,
} from "@mui/material";
import React, { useEffect, useState } from "react";
import AddBoxIcon from "@mui/icons-material/AddBox";
import { useQueryClient } from "@tanstack/react-query";
Expand All @@ -9,6 +19,7 @@ import CodeHooks from "../../../api/CodeHooks";
import { QueryKey } from "../../../api/QueryKey";
import { ErrorMessage } from "@hookform/error-message";
import { LoadingButton } from "@mui/lab";
import { HexColorPicker } from "react-colorful";

interface CodeDialogProps {
projectId: number;
Expand All @@ -23,17 +34,21 @@ export default function CodeCreationDialog({ projectId, userId, codes }: CodeDia
handleSubmit,
formState: { errors },
reset,
setValue,
} = useForm();

// state
// local state
const [open, setOpen] = useState(false); // state of the dialog, either open or closed
const [selectedParent, setSelectedParent] = useState(-1);
const [color, setColor] = useState("#000000");

// initialize state properly
useEffect(() => {
setSelectedParent(-1);
setColor("#000000");
reset();
}, [reset]);
setValue("color", "#000000");
}, [setValue, reset]);

// mutations
const queryClient = useQueryClient();
Expand All @@ -52,7 +67,11 @@ export default function CodeCreationDialog({ projectId, userId, codes }: CodeDia
text: `Added code ${data.name}`,
severity: "success",
});
reset(); // reset form

// reset
reset();
setColor("#000000");
setValue("color", "#000000");
},
});

Expand Down Expand Up @@ -106,13 +125,26 @@ export default function CodeCreationDialog({ projectId, userId, codes }: CodeDia
error={Boolean(errors.name)}
helperText={<ErrorMessage errors={errors} name="name" />}
/>
<TextField
label="Color"
fullWidth
variant="standard"
{...register("color", { required: "Color is required" })}
error={Boolean(errors.color)}
helperText={<ErrorMessage errors={errors} name="color" />}
<Stack direction="row">
<TextField
label="Color"
fullWidth
variant="standard"
{...register("color", { required: "Color is required" })}
onChange={(e) => setColor(e.target.value)}
error={Boolean(errors.color)}
helperText={<ErrorMessage errors={errors} name="color" />}
InputLabelProps={{ shrink: true }}
/>
<Box sx={{ width: 48, height: 48, backgroundColor: color, ml: 1, flexShrink: 0 }} />
</Stack>
<HexColorPicker
style={{ width: "100%" }}
color={color}
onChange={(newColor) => {
setValue("color", newColor); // set value of text input
setColor(newColor); // set value of color picker (and box)
}}
/>
<TextField
multiline
Expand Down
40 changes: 29 additions & 11 deletions frontend/src/views/annotation/CodeExplorer/CodeEditDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField } from "@mui/material";
import { Box, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField } from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import SnackbarAPI from "../../../features/snackbar/SnackbarAPI";
Expand All @@ -9,6 +9,8 @@ import CodeHooks from "../../../api/CodeHooks";
import { QueryKey } from "../../../api/QueryKey";
import { ErrorMessage } from "@hookform/error-message";
import { LoadingButton } from "@mui/lab";
import { HexColorPicker } from "react-colorful";
import ColorUtils from "../../../utils/ColorUtils";

interface CodeEditDialogProps {
codes: CodeRead[];
Expand All @@ -21,12 +23,14 @@ function CodeEditDialog({ codes }: CodeEditDialogProps) {
handleSubmit,
formState: { errors },
reset,
setValue,
} = useForm();

// state
// local state
const [code, setCode] = useState<CodeRead | null>(null);
const [open, setOpen] = useState(false);
const [selectedParent, setSelectedParent] = useState(-1);
const [color, setColor] = useState("#000000");

// listen to event
// create a (memoized) function that stays the same across re-renders
Expand All @@ -45,12 +49,14 @@ function CodeEditDialog({ codes }: CodeEditDialogProps) {
// initialize form when code changes
useEffect(() => {
if (code) {
const c = ColorUtils.rgbStringToHex(code.color) || code.color;
reset({
name: code.name,
description: code.description,
color: code.color,
color: c,
});
setSelectedParent(!code.parent_code_id ? -1 : code.parent_code_id);
setColor(c);
}
}, [code, reset]);

Expand Down Expand Up @@ -115,7 +121,6 @@ function CodeEditDialog({ codes }: CodeEditDialogProps) {
</MenuItem>
))}
</TextField>

<TextField
label="Name"
fullWidth
Expand All @@ -124,13 +129,26 @@ function CodeEditDialog({ codes }: CodeEditDialogProps) {
error={Boolean(errors.name)}
helperText={<ErrorMessage errors={errors} name="name" />}
/>
<TextField
label="Color"
fullWidth
variant="standard"
{...register("color", { required: "Color is required" })}
error={Boolean(errors.color)}
helperText={<ErrorMessage errors={errors} name="color" />}
<Stack direction="row">
<TextField
label="Color"
fullWidth
variant="standard"
{...register("color", { required: "Color is required" })}
onChange={(e) => setColor(e.target.value)}
error={Boolean(errors.color)}
helperText={<ErrorMessage errors={errors} name="color" />}
InputLabelProps={{ shrink: true }}
/>
<Box sx={{ width: 48, height: 48, backgroundColor: color, ml: 1, flexShrink: 0 }} />
</Stack>
<HexColorPicker
style={{ width: "100%" }}
color={color}
onChange={(newColor) => {
setValue("color", newColor); // set value of text input
setColor(newColor); // set value of color picker (and box)
}}
/>
<TextField
multiline
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { forwardRef, useImperativeHandle, useState } from "react";
import {
Box,
Button,
Dialog,
DialogActions,
Expand All @@ -20,6 +21,7 @@ import SnackbarAPI from "../../../features/snackbar/SnackbarAPI";
import { QueryKey } from "../../../api/QueryKey";
import { CodeRead } from "../../../api/openapi";
import { useAppSelector } from "../../../plugins/ReduxHooks";
import { HexColorPicker } from "react-colorful";

interface CodeCreationDialogProps {
onCreateSuccess: (code: CodeRead) => void;
Expand All @@ -41,6 +43,7 @@ const CodeCreationDialog = forwardRef<CodeCreationDialogHandle, CodeCreationDial

// local state
const [isCodeCreateDialogOpen, setIsCodeCreateDialogOpen] = useState(false);
const [color, setColor] = useState("#000000");

// react form
const {
Expand Down Expand Up @@ -80,12 +83,15 @@ const CodeCreationDialog = forwardRef<CodeCreationDialogHandle, CodeCreationDial

// methods
const openCodeCreateDialog = (name?: string) => {
// reset
reset();
setValue("name", name ? name : "");
setValue("color", "#000000");
setColor("#000000");
setIsCodeCreateDialogOpen(true);
};

const closeCodeCreateDialog = () => {
reset();
setIsCodeCreateDialogOpen(false);
};

Expand Down Expand Up @@ -134,13 +140,26 @@ const CodeCreationDialog = forwardRef<CodeCreationDialogHandle, CodeCreationDial
error={Boolean(errors.name)}
helperText={<ErrorMessage errors={errors} name="name" />}
/>
<TextField
label="Color"
fullWidth
variant="standard"
{...register("color", { required: "Color is required" })}
error={Boolean(errors.color)}
helperText={<ErrorMessage errors={errors} name="color" />}
<Stack direction="row">
<TextField
label="Color"
fullWidth
variant="standard"
{...register("color", { required: "Color is required" })}
onChange={(e) => setColor(e.target.value)}
error={Boolean(errors.color)}
helperText={<ErrorMessage errors={errors} name="color" />}
InputLabelProps={{ shrink: true }}
/>
<Box sx={{ width: 48, height: 48, backgroundColor: color, ml: 1, flexShrink: 0 }} />
</Stack>
<HexColorPicker
style={{ width: "100%" }}
color={color}
onChange={(newColor) => {
setValue("color", newColor); // set value of text input
setColor(newColor); // set value of color picker (and box)
}}
/>
<TextField
multiline
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/search/DocumentViewer/ImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function ImageViewer({ sdoc, adoc, showEntities }: ImageViewerProps) {
return (
<>
{annotations.isError && <span>{annotations.error.message}</span>}
<svg ref={svgRef} onClick={() => console.log("TEST")} width="100%" height="100%">
<svg ref={svgRef} width="100%" height="100%" style={{ cursor: "move" }}>
<g ref={gRef}>
<image href={sdoc.content} />
<g ref={bboxRef}></g>
Expand Down
34 changes: 33 additions & 1 deletion frontend/src/views/search/Tags/TagEdit/TagEditDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField } from "@mui/material";
import { Box, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField } from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import SnackbarAPI from "../../../../features/snackbar/SnackbarAPI";
Expand All @@ -9,6 +9,8 @@ import TagHooks from "../../../../api/TagHooks";
import { QueryKey } from "../../../../api/QueryKey";
import { ErrorMessage } from "@hookform/error-message";
import { LoadingButton } from "@mui/lab";
import { HexColorPicker } from "react-colorful";
import ColorUtils from "../../../../utils/ColorUtils";

/**
* A dialog that allows to update a DocumentTag.
Expand All @@ -23,11 +25,13 @@ function TagEditDialog() {
handleSubmit,
formState: { errors },
reset,
setValue,
} = useForm();

// state
const [tagId, setTagId] = useState<number | undefined>(undefined);
const [open, setOpen] = useState(false);
const [color, setColor] = useState("#000000");

// query
const tag = useQuery<DocumentTagRead, Error>(
Expand All @@ -54,10 +58,13 @@ function TagEditDialog() {
// initialize form when tag changes
useEffect(() => {
if (tag.data) {
const c = ColorUtils.rgbStringToHex(tag.data.description) || tag.data.description;
reset({
title: tag.data.title,
description: tag.data.description,
color: c,
});
setColor(c);
}
}, [tag.data, reset]);

Expand Down Expand Up @@ -113,6 +120,31 @@ function TagEditDialog() {
helperText={<>{errors?.title ? errors.title.message : ""}</>}
disabled={!tag.isSuccess}
/>
<Stack direction="row">
<TextField
label="Color"
fullWidth
variant="standard"
{...register("color", { required: "Color is required" })}
onChange={(e) => {
setColor(e.target.value);
setValue("description", e.target.value); // todo: remove this hack once tag has color attribute
}}
error={Boolean(errors.color)}
helperText={<ErrorMessage errors={errors} name="color" />}
InputLabelProps={{ shrink: true }}
/>
<Box sx={{ width: 48, height: 48, backgroundColor: color, ml: 1, flexShrink: 0 }} />
</Stack>
<HexColorPicker
style={{ width: "100%" }}
color={color}
onChange={(newColor) => {
setValue("color", newColor); // set value of text input
setColor(newColor); // set value of color picker (and box)
setValue("description", newColor); // todo: remove this hack once tag has color attribute
}}
/>
<TextField
multiline
minRows={5}
Expand Down

0 comments on commit a7b0680

Please sign in to comment.