Skip to content

Commit 783f81c

Browse files
committed
feat: add image functionality
Signed-off-by: ayushnirwal <53055971+ayushnirwal@users.noreply.github.com>
1 parent 75932c9 commit 783f81c

8 files changed

Lines changed: 111 additions & 4 deletions

File tree

packages/frappe-ui-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@tiptap/extension-code-block-lowlight": "^3.17.1",
4747
"@tiptap/extension-highlight": "^3.17.1",
4848
"@tiptap/extension-horizontal-rule": "^3.17.1",
49+
"@tiptap/extension-image": "^3.19.0",
4950
"@tiptap/extension-list": "^3.17.1",
5051
"@tiptap/extension-placeholder": "^3.17.1",
5152
"@tiptap/extension-strike": "^3.17.1",

packages/frappe-ui-react/src/components/textEditor/menu/commands/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
TableIcon,
2626
TypeIcon,
2727
Undo2Icon,
28+
Image,
2829
} from "lucide-react";
2930

3031
/**
@@ -264,6 +265,17 @@ export const COMMANDS: Record<TYPE_COMMANDS_KEYS, EditorCommand> = {
264265
isDisabled: (editor) => !editor.can().redo(),
265266
isActive: () => false,
266267
},
268+
select_image: {
269+
label: "Image",
270+
icon: Image,
271+
action: () => {
272+
const inpEle = document.getElementById("fileInput");
273+
if (inpEle) {
274+
inpEle.click();
275+
}
276+
},
277+
isActive: () => false,
278+
},
267279
};
268280

269281
export default COMMANDS;

packages/frappe-ui-react/src/components/textEditor/menu/commands/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const COMMANDS_KEYS = [
3939
"redo",
4040
"codeblock",
4141
"horizontal_rule",
42+
"select_image",
4243
] as const;
4344

4445
export type TYPE_COMMANDS_KEYS = (typeof COMMANDS_KEYS)[number];

packages/frappe-ui-react/src/components/textEditor/menu/menu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const DEFAULT_COMMANDS: Array<
6060
"undo",
6161
"redo",
6262
"horizontal_rule",
63+
"select_image",
6364
];
6465

6566
const Menu = ({ className }: MenuProps) => {

packages/frappe-ui-react/src/components/textEditor/textEditor.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import HorizontalRule from "@tiptap/extension-horizontal-rule";
1212
import Strike from "@tiptap/extension-strike";
1313
import Placeholder from "@tiptap/extension-placeholder";
1414
import { TableKit } from "@tiptap/extension-table";
15+
import Image from "@tiptap/extension-image";
1516
import clsx from "clsx";
17+
import type { ChangeEventHandler } from "react";
1618

1719
/**
1820
* Internal dependencies.
@@ -22,6 +24,7 @@ import { normalizeClasses } from "../../utils";
2224
import type { TextEditorProps } from "./types";
2325
import FixedMenu from "./menu/fixedMenu";
2426
import { ExtendedCodeBlock } from "./extension/codeBlock";
27+
import { getBase64File } from "./utils/getBase64File";
2528

2629
const TextEditor = ({
2730
content,
@@ -39,6 +42,7 @@ const TextEditor = ({
3942
Top,
4043
Editor,
4144
Bottom,
45+
uploadFunction = getBase64File,
4246
}: TextEditorProps) => {
4347
const editor = useEditor(
4448
{
@@ -83,6 +87,12 @@ const TextEditor = ({
8387
},
8488
}),
8589
ExtendedCodeBlock,
90+
Image.configure({
91+
allowBase64: true,
92+
resize: {
93+
enabled: true,
94+
},
95+
}),
8696
...extensions,
8797
],
8898
onUpdate: ({ editor }) => {
@@ -112,8 +122,34 @@ const TextEditor = ({
112122
]
113123
);
114124

125+
const handleUpload: ChangeEventHandler<HTMLInputElement> = async (e) => {
126+
const file = e.target.files?.[0];
127+
if (!file || !editor) return;
128+
if (!file.type.startsWith("image/")) {
129+
return;
130+
}
131+
132+
const res = await uploadFunction(file);
133+
const url = res.file_url;
134+
135+
if (url) {
136+
editor.chain().focus().setImage({ src: url }).run();
137+
}
138+
139+
e.target.value = "";
140+
};
141+
115142
return (
116143
<EditorContext.Provider value={{ editor }}>
144+
{/* Invisible field to provide upload functionality */}
145+
<input
146+
id="fileInput"
147+
type="file"
148+
style={{
149+
visibility: "hidden",
150+
}}
151+
onChange={handleUpload}
152+
/>
117153
{Top && <Top />}
118154
{fixedMenu && <FixedMenu />}
119155
{Editor ? <Editor editor={editor} /> : <EditorContent editor={editor} />}

packages/frappe-ui-react/src/components/textEditor/types.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,22 @@
33
*/
44
import { Editor, type Extension } from "@tiptap/react";
55
import type { StarterKitOptions } from "@tiptap/starter-kit";
6-
import type { FC } from "react";
6+
7+
export interface UploadedFile {
8+
file_name: string;
9+
file_size: number;
10+
file_url: string;
11+
name?: string;
12+
owner?: string;
13+
creation?: string;
14+
modified?: string;
15+
modified_by?: string;
16+
is_private?: 0 | 1;
17+
file_type?: string;
18+
folder?: string;
19+
is_folder?: 0 | 1;
20+
content_hash?: string;
21+
}
722

823
export interface TextEditorProps {
924
// Props
@@ -21,9 +36,11 @@ export interface TextEditorProps {
2136
onBlur?: (event: FocusEvent) => void;
2237
onTransaction?: (editor: Editor) => void;
2338
// Slots
24-
Top?: FC;
25-
Editor?: FC<{ editor: Editor }>;
26-
Bottom?: FC;
39+
Top?: React.FC;
40+
Editor?: React.FC<{ editor: Editor }>;
41+
Bottom?: React.FC;
42+
// handlers
43+
uploadFunction?: (file: File) => Promise<UploadedFile>;
2744
}
2845

2946
export interface EditorCommand {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* External dependencies.
3+
*/
4+
import type { UploadedFile } from "../types";
5+
6+
export const getBase64File = async (file: File): Promise<UploadedFile> => {
7+
const base64Url = await new Promise<string>((resolve, reject) => {
8+
const reader = new FileReader();
9+
reader.onload = (event) => {
10+
const result = event.target?.result;
11+
if (typeof result === "string") {
12+
resolve(result);
13+
} else {
14+
reject(new Error("Failed to read file as base64"));
15+
}
16+
};
17+
reader.onerror = () => {
18+
reject(new Error("Error reading file"));
19+
};
20+
reader.readAsDataURL(file);
21+
});
22+
return {
23+
file_name: file.name,
24+
file_size: file.size,
25+
file_url: base64Url,
26+
};
27+
};

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)