Skip to content

Commit

Permalink
Features/upload image icon (#55)
Browse files Browse the repository at this point in the history
* feat: refactor mention

* refactor image

* feat: add upload when clicking the menu icon

* feat: reorder and refactor menus

* Update TextEditor.tsx

* Update Toolbar.tsx

* Create UploadFile.tsx

* Delete UploadImage.tsx

* Update App.tsx

* Update CustomImage.tsx

* Update useTextEditor.ts

* Update types.d.ts

* Update app.utils.ts

* Update README.md

* change input min height

* Update App.tsx

* remove unused styles

* fix disable button tooltip child

* fix fill rule proprerties using camel case

* use span instead of div for icons

* Update App.tsx

* refactor image alt editor

* add legend to image

* add legend types,messages

* check image dimensions
  • Loading branch information
tiavina-mika authored Jul 27, 2024
1 parent 1a99ef2 commit 6a4ee81
Show file tree
Hide file tree
Showing 18 changed files with 850 additions and 476 deletions.
142 changes: 61 additions & 81 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ A customizable and easy to use <a href="https://tiptap.dev/">Tiptap</a> styled W
- [Props](#props)
- [Image upload props `ImageUploadOptions`](#image-upload-props-imageuploadoptions)
- [New features](#new-features)
- [v0.9.9](#v099)
- [v0.9.6](#v096)
- [v0.9.0](#v090)
- [Contributing](#contributing)

</details>
Expand Down Expand Up @@ -112,36 +109,40 @@ function App() {
);
}
```
### Image upload

![Gif](https://github.com/tiavina-mika/mui-tiptap-editor/blob/main/screenshots/image-upload.gif)

### Image upload
<ul>
<li>The image can be uploaded to the server via an API call or directly into the content as base64 string. </li>
<li>For the moment we can only upload via drag and drop and copy paste.</li>
<li>Add or modify the alt text of the image</li>
<li>The image can be uploaded using upload button or pasted or dropped.</li>
<li>Add or modify the alt text and the legend (title) of the image</li>
<li>Delete the selected image using `Delete` keyboard key</li>
</ul>

```tsx
// example of API using axios
// note that the response should be directly the image url
// so it can be displayed in the editor
function uploadImage(file) {
const data = new FormData();
data.append('file', file);
const response = axios.post('/documents/image/upload', data);
return response.data;
// example of API upload using fetch
// the return data must be the image url (string) or image attributes (object) like src, alt, id, title, ...
const uploadFile = async (file: File) => {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("https://api.escuelajs.co/api/v1/files/upload", {
method: "POST",
body: formData,
});
const data = await response.json();
// or return data.location
return { id: data.filename, src: data.location };
};

function App() {
return (
<TextEditor
uploadImageOptions={{
uploadImage: uploadImage, // the image is stored and used as base64 string if not specified
uploadFileOptions={{
uploadFile, // the image is stored and used as base64 string if not specified
maxSize: 5, // max size to 10MB if not specified
maxFilesNumber: 2, // max 5 files if not specified
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/jpg'], // all image types if not specified
imageMaxWidth: 400, // default to 1920
imageMaxHeight: 400, // default to 1080
}}
/>
);
Expand All @@ -161,7 +162,7 @@ import { TextEditorReadOnly } from 'mui-tiptap-editor';

2. If it is just displaying the value without using the editor, you can use this library [`tiptap-parser`](https://www.npmjs.com/package/tiptap-parser). Example: The editor is used in the back office, but the content must be displayed on the website
```tsx
<TiptapParser content={`<h1>Hello world</h1>`} />
<TiptapParser content="<h1>Hello world</h1>" />
```

## Customization
Expand All @@ -183,72 +184,31 @@ import { TextEditorReadOnly } from 'mui-tiptap-editor';
},
toolbar: {
bold: "Gras",
italic: "Italique",
strike: "Barré",
underline: "Souligné",
link: "Lien",
bulletList: "Liste à puces",
orderedList: "Liste ordonnée",
alignLeft: "Aligner à gauche",
alignCenter: "Aligner au centre",
alignRight: "Aligner à droite",
alignJustify: "Justifier",
blockquote: "Citation",
codeBlock: "Code",
table: "Table",
youtube: "Youtube",
undo: "Annuler",
redo: "Refaire",
mention: "Mention"
upload: "Ajouter une image",
// ...
},
headings: {
normalText: "Text normal",
h1: "En-tête 1",
h2: "En-tête 2",
h3: "En-tête 3",
h4: "En-tête 4",
h5: "En-tête 5",
h6: "En-tête 6"
// ...
},
table: {
table: "Tableau",
addColumnBefore: "Ajouter une colonne avant",
addColumnAfter: "Ajouter une colonne après",
deleteColumn: "Supprimer la colonne",
addRowBefore: "Ajouter une ligne avant",
addRowAfter: "Ajouter une ligne après",
deleteRow: "Supprimer la ligne",
mergeCells: "Fusionner les cellules",
splitCell: "Diviser la cellule",
deleteTable: "Supprimer le tableau",
insertTable: "Insérer un tableau",
toggleHeaderCell: "Basculer la cellule d'en-tête",
toggleHeaderColumn: "Basculer la colonne d'en-tête",
toggleHeaderRow: "Basculer la ligne d'en-tête",
mergeOrSplit: "Fusionner ou diviser",
setCellAttribute: "Définir l'attribut de cellule"
// ....
},
link: {
link: "Lien",
insert: "Insérer le lien",
invalid: "Lien invalide",
// ...
},
youtube: {
link: "Lien",
insert: "Insérer la vidéo Youtube",
title: "Insérer une vidéo Youtube",
invalid: "Lien invalide",
enter: "Entrer le lien",
height: "Hauteur",
width: "Largeur"
},
imageUpload: {
upload: {
fileTooLarge: "Fichier trop volumineux",
maximumNumberOfFiles: "Nombre maximum de fichiers atteint",
enterValidAltText: "Entrez un texte alternatif valide",
addAltText: "Ajouter un texte alternatif",
invalidMimeType: "Type de fichier invalide",
},
// ...
}
}}
/>
```
Expand Down Expand Up @@ -314,28 +274,48 @@ import './index.css';
|onChange|`(value: string) => void`|-| Function to call when the input change
|userPathname|`string`|/user| URL pathname for the mentioned user (eg: /user/user_id)
|labels|`ILabels`|null| Override labels, for example using `i18n`
|uploadImageOptions|`ImageUploadOptions`|null| Override image upload default options like max size, max file number, ...
|uploadFileOptions|`ImageUploadOptions`|null| Override image upload default options like max size, max file number, ...
|...all tiptap features|[EditorOptions](https://github.com/ueberdosis/tiptap/blob/e73073c02069393d858ca7d8c44b56a651417080/packages/core/src/types.ts#L52)|empty| Can access to all tiptap `useEditor` props

See [`here`](https://github.com/tiavina-mika/mui-tiptap-editor/blob/main/src/dev/App.tsx) how to use all the `TextEditor` props.

## Image upload props `ImageUploadOptions`
|props |type | Default value | Description |
|----------------|-------------------------------|-----------------------------|-----------------------------|
|uploadImage|`function`|undefined|an API call to your server to handle and store the image
|uploadFile|`function`|undefined|an API call to your server to handle and store the image
|maxSize|`number`|10|maximum size of the image in MB
|maxFilesNumber|`number`|5|maximum number of files to be uploaded at once
|allowedMimeTypes|`string[]`|all image types|allowed mime types to be uploaded


|allowedMimeTypes|`string[]`|null|all image types|allowed mime types to be uploaded
|imageMaxWidth|`number`|1920|maximum width of the image
|imageMaxHeight|`number`|1080|maximum height of the image

## New features
#### [v0.9.9](https://github.com/tiavina-mika/mui-tiptap-editor/pull/52)
- Upload image via drop or paste
- Add or edit the image alt text

#### v0.9.6
- Work with dark mode
#### v0.9.0
- Customize and override all messages and labels

<table>
<tr>
<td>Versions<td>
<td>Features</td>
<tr>
<tr>
<th><a href="https://github.com/tiavina-mika/mui-tiptap-editor/pull/52">v0.9.11</a><th>
<td>
<ul>
<li>Upload image via drop, paste or upload button</li>
<li>Add or edit the image alt text and legend</li>
<li>Reorder toolbar menus</li>
</ul>
</td>
<tr>
<tr>
<th><a href="https://github.com/tiavina-mika/mui-tiptap-editor/pull/52">v0.9.9</a><th>
<td>
<ul>
<li>Work with dark mode</li>
<li>Customize and override all messages and labels</li>
</ul>
</td>
<tr>
</table>

## Contributing

Expand Down
Binary file removed screenshots/image-upload.gif
Binary file not shown.
40 changes: 19 additions & 21 deletions src/components/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Tooltip, useTheme } from "@mui/material";
import { useTheme } from "@mui/material";
import { Editor } from "@tiptap/react";

import { ChangeEvent, useEffect, useState } from "react";
import Icon from "../icons/Icon";
import TextColor from "../icons/TextColor";

const classes = {
color: {
Expand All @@ -26,55 +24,55 @@ const classes = {
}
},
colorPreview: (color: string) => ({
position: 'absolute' as const,
bottom: 10,
height: 3,
width: 18,
width: 14,
backgroundColor: color,
marginTop: -8,
borderRadius: 3,
})
};
type Props = {
editor: Editor;
id: string;
};
const ColorPicker = ({ editor }: Props) => {
const ColorPicker = ({ editor, id }: Props) => {
const [color, setColor] = useState<string>("");

const theme = useTheme();

// add default styles if not defined
useEffect(() => {
// get current color from editor instance
const currentColor = editor.getAttributes("textStyle").color;
// set default color based on theme
const defaultColor = theme.palette.mode === "dark" ? "#ffffff" : "#000000";
setColor(currentColor || defaultColor);
}, [editor, theme.palette.mode])

const handleInput = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
// set color in editor instance
editor.chain().focus().setColor(value).run();
setColor(value);
};

return (
<div className="flexRow center stretchSelf">
{/* tooltip */}
<Tooltip title="Text color">
<label htmlFor="color-picker" className="flexCenter">
<Icon size={28} css={{ cursor: 'pointer' }}>
<TextColor />
<div css={classes.colorPreview(color)} />
</Icon>
</label>
</Tooltip>

{/* input */}
<>
<input
id="color-picker"
id={id}
type="color"
onInput={handleInput}
value={color}
css={classes.color}
// css={classes.color}
css={{ display: 'none' }}
/>
</div>
{/*
* The `colorPreview` div displays the selected color as a small rectangle below the color picker.
* see Toolbar component for the implementation of icon
*/}
<div css={classes.colorPreview(color)} />
</>
);
};

Expand Down
15 changes: 6 additions & 9 deletions src/components/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const classes = {
border: isActive ? "0px solid gray !important" : "none !important",
borderRight: `1px solid ${theme.palette.grey[300]}`,
fontSize: 14,
lineHeight: 1,
cursor: 'pointer',
'& span': {
marginRight: 10
Expand All @@ -49,9 +50,7 @@ type Props = {
headingLabels?: ILabels["headings"];
};
const Heading = ({ editor, headingLabels }: Props) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(
null
);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [selected, setSelected] = useState(0);

// get label for selected heading
Expand All @@ -75,9 +74,7 @@ const Heading = ({ editor, headingLabels }: Props) => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};
const handleClose = () => setAnchorEl(null);

const handleSelectHeading = (heading: Level) => {
editor.chain().focus().toggleHeading({ level: heading }).run();
Expand All @@ -92,7 +89,7 @@ const Heading = ({ editor, headingLabels }: Props) => {
}

return (
<div>
<span>
{/* button */}
<Button
type="button"
Expand All @@ -104,7 +101,7 @@ const Heading = ({ editor, headingLabels }: Props) => {
>
<span>
{selectedLabel}
</span>
</span>
{/* chevron icon */}
<Icon>
<ChevronDown />
Expand Down Expand Up @@ -140,7 +137,7 @@ const Heading = ({ editor, headingLabels }: Props) => {
</MenuItem>
))}
</Menu>
</div>
</span>

);
};
Expand Down
Loading

0 comments on commit 6a4ee81

Please sign in to comment.