Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/frappe-ui-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@tiptap/extension-code-block-lowlight": "^3.17.1",
"@tiptap/extension-highlight": "^3.17.1",
"@tiptap/extension-horizontal-rule": "^3.17.1",
"@tiptap/extension-image": "^3.19.0",
"@tiptap/extension-list": "^3.17.1",
"@tiptap/extension-placeholder": "^3.17.1",
"@tiptap/extension-strike": "^3.17.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* External dependencies.
*/
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { createLowlight, all } from "lowlight";

/**
* Internal dependencies.
*/
import { getCodeBlockCtx, INDENT, lineStartsBetween } from "./utils";
import { ReactNodeViewRenderer } from "@tiptap/react";
import CodeBlockComponent from "./codeBlockNode";

const lowlight = createLowlight(all);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
TableIcon,
TypeIcon,
Undo2Icon,
Image,
} from "lucide-react";

/**
Expand Down Expand Up @@ -264,6 +265,17 @@ export const COMMANDS: Record<TYPE_COMMANDS_KEYS, EditorCommand> = {
isDisabled: (editor) => !editor.can().redo(),
isActive: () => false,
},
select_image: {
label: "Image",
icon: Image,
action: () => {
const inpEle = document.getElementById("fileInput");
if (inpEle) {
inpEle.click();
}
},
isActive: () => false,
},
};

export default COMMANDS;
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const COMMANDS_KEYS = [
"redo",
"codeblock",
"horizontal_rule",
"select_image",
] as const;

export type TYPE_COMMANDS_KEYS = (typeof COMMANDS_KEYS)[number];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const DEFAULT_COMMANDS: Array<
"undo",
"redo",
"horizontal_rule",
"select_image",
];

const Menu = ({ className }: MenuProps) => {
Expand Down
34 changes: 34 additions & 0 deletions packages/frappe-ui-react/src/components/textEditor/textEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,37 @@ img.ProseMirror-selectednode {
.tag-suggestion-active {
background-color: var(--color-surface-gray-2, #f3f3f3);
}


[data-resize-handle] {
position: absolute;
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 2px;
z-index: 10;

&:hover {
background: rgba(0, 0, 0, 0.8);
}

/* Corner handles */
&[data-resize-handle='top-left'],
&[data-resize-handle='top-right'],
&[data-resize-handle='bottom-left'],
&[data-resize-handle='bottom-right'] {
width: 8px;
height: 8px;
}

&[data-resize-handle='bottom-right'] {
bottom: -4px;
right: -4px;
cursor: nwse-resize;
}

}

[data-resize-state='true'] [data-resize-wrapper] {
outline: 1px solid rgba(0, 0, 0, 0.25);
border-radius: 0.125rem;
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,13 @@ const CONTENT: string = `
<p>
This is a paragraph with <strong>bold</strong> and <em>italic</em> text.
</p>
<img src="https://placehold.co/600x400" />
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<pre><code class="language-javascript">import { Button } from '@rtcamp/frappe-ui-react'
const value = ref(true);</code></pre>
</ul>
</div>
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import HorizontalRule from "@tiptap/extension-horizontal-rule";
import Strike from "@tiptap/extension-strike";
import Placeholder from "@tiptap/extension-placeholder";
import { TableKit } from "@tiptap/extension-table";
import Image from "@tiptap/extension-image";
import clsx from "clsx";
import type { ChangeEventHandler } from "react";

/**
* Internal dependencies.
Expand All @@ -21,7 +23,8 @@ import "./textEditor.css";
import { normalizeClasses } from "../../utils";
import type { TextEditorProps } from "./types";
import FixedMenu from "./menu/fixedMenu";
import { ExtendedCodeBlock } from "./extension/codeBlock";
import { ExtendedCodeBlock } from "./extension/codeBlock/codeBlock";
import { getBase64File } from "./utils/getBase64File";

const TextEditor = ({
content,
Expand All @@ -39,6 +42,7 @@ const TextEditor = ({
Top,
Editor,
Bottom,
uploadFunction = getBase64File,
}: TextEditorProps) => {
const editor = useEditor(
{
Expand Down Expand Up @@ -83,6 +87,16 @@ const TextEditor = ({
},
}),
ExtendedCodeBlock,
Image.configure({
allowBase64: true,
resize: {
enabled: true,
directions: ["bottom-right"], // can be any direction or diagonal combination
minWidth: 50,
minHeight: 50,
alwaysPreserveAspectRatio: true,
},
}),
...extensions,
],
onUpdate: ({ editor }) => {
Expand Down Expand Up @@ -112,8 +126,34 @@ const TextEditor = ({
]
);

const handleUpload: ChangeEventHandler<HTMLInputElement> = async (e) => {
const file = e.target.files?.[0];
if (!file || !editor) return;
if (!file.type.startsWith("image/")) {
return;
}

const res = await uploadFunction(file);
const url = res.file_url;

if (url) {
editor.chain().focus().setImage({ src: url }).run();
}

e.target.value = "";
};

return (
<EditorContext.Provider value={{ editor }}>
{/* Invisible field to provide upload functionality */}
<input
id="fileInput"
type="file"
style={{
visibility: "hidden",
}}
onChange={handleUpload}
/>
{Top && <Top />}
{fixedMenu && <FixedMenu />}
{Editor ? <Editor editor={editor} /> : <EditorContent editor={editor} />}
Expand Down
25 changes: 21 additions & 4 deletions packages/frappe-ui-react/src/components/textEditor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@
*/
import { Editor, type Extension } from "@tiptap/react";
import type { StarterKitOptions } from "@tiptap/starter-kit";
import type { FC } from "react";

export interface UploadedFile {
file_name: string;
file_size: number;
file_url: string;
name?: string;
owner?: string;
creation?: string;
modified?: string;
modified_by?: string;
is_private?: 0 | 1;
file_type?: string;
folder?: string;
is_folder?: 0 | 1;
content_hash?: string;
}

export interface TextEditorProps {
// Props
Expand All @@ -21,9 +36,11 @@ export interface TextEditorProps {
onBlur?: (event: FocusEvent) => void;
onTransaction?: (editor: Editor) => void;
// Slots
Top?: FC;
Editor?: FC<{ editor: Editor }>;
Bottom?: FC;
Top?: React.FC;
Editor?: React.FC<{ editor: Editor }>;
Bottom?: React.FC;
// handlers
uploadFunction?: (file: File) => Promise<UploadedFile>;
}

export interface EditorCommand {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* External dependencies.
*/
import type { UploadedFile } from "../types";

export const getBase64File = async (file: File): Promise<UploadedFile> => {
const base64Url = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result;
if (typeof result === "string") {
resolve(result);
} else {
reject(new Error("Failed to read file as base64"));
}
};
reader.onerror = () => {
reject(new Error("Error reading file"));
};
reader.readAsDataURL(file);
});
return {
file_name: file.name,
file_size: file.size,
file_url: base64Url,
};
};
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.