Skip to content

Commit 4c21848

Browse files
committed
feat(admin): add upload image to milkdown editor
1 parent e58d186 commit 4c21848

File tree

1 file changed

+211
-23
lines changed

1 file changed

+211
-23
lines changed
Lines changed: 211 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,220 @@
11
console.log('🌊')
2+
23
import { Crepe } from "@milkdown/crepe";
34
import "@milkdown/crepe/theme/common/style.css";
45
import "@milkdown/crepe/theme/frame.css";
6+
import { upload, uploadConfig } from "@milkdown/kit/plugin/upload";
7+
import type { Uploader } from "@milkdown/kit/plugin/upload";
8+
import type { Node } from "@milkdown/kit/prose/model";
9+
// Function to convert a blob URL to a file
10+
async function blobUrlToFile(blobUrl: string): Promise<File> {
11+
try {
12+
const response = await fetch(blobUrl);
13+
if (!response.ok) {
14+
throw new Error(`Failed to fetch blob: ${response.status} ${response.statusText}`);
15+
}
16+
const blob = await response.blob();
17+
const filename = `image-${Date.now()}`;
18+
return new File([blob], filename, { type: blob.type });
19+
} catch (error) {
20+
console.error("Error converting blob URL to file:", error);
21+
throw error;
22+
}
23+
}
24+
// Function to convert a base64 image to a file
25+
async function base64ToFile(base64String: string): Promise<File> {
26+
try {
27+
// Extract MIME type and base64 data
28+
const matches = base64String.match(/^data:(image\/\w+);base64,(.+)$/);
29+
if (!matches || matches.length !== 3) {
30+
throw new Error("Invalid base64 image format");
31+
}
32+
const mimeType = matches[1];
33+
const base64Data = matches[2];
34+
// Convert base64 to blob
35+
const byteCharacters = atob(base64Data);
36+
const byteArrays = [];
37+
for (let i = 0; i < byteCharacters.length; i++) {
38+
byteArrays.push(byteCharacters.charCodeAt(i));
39+
}
40+
const byteArray = new Uint8Array(byteArrays);
41+
const blob = new Blob([byteArray], {type: mimeType});
42+
// Create a file
43+
const filename = `image-${Date.now()}`;
44+
return new File([blob], filename, { type: mimeType });
45+
} catch (error) {
46+
console.error("Error converting base64 to file:", error);
47+
throw error;
48+
}
49+
}
550

6-
document.addEventListener("DOMContentLoaded", () => {
7-
const editorElement = document.querySelector("#editor");
8-
const markdownContent = editorElement.getAttribute("data-markdown") || "";
51+
const uploadImageToServer = async (file: File): Promise<string> => {
52+
console.log("Uploading image to server:", file.name);
53+
54+
// Retrieve the CSRF token from the hidden input
55+
const csrfTokenElement = document.querySelector('input[name="#csrf_token"]') as HTMLInputElement;
56+
if (!csrfTokenElement) {
57+
throw new Error("CSRF token not found in the page");
58+
}
59+
const csrfToken = csrfTokenElement.value;
960

10-
const crepe = new Crepe({
11-
root: "#editor",
12-
defaultValue: markdownContent,
13-
});
61+
// Create the FormData and add the file and CSRF token
62+
const formData = new FormData();
63+
formData.append("file", file);
64+
formData.append("#csrf_token", csrfToken); // Key corresponding to Session::CSRF_TOKEN_KEY
1465

15-
crepe.create().then(() => {
16-
console.log("Milkdown editor initialised with existing Markdown content");
66+
try {
67+
const response = await fetch("/admin/upload-image", {
68+
method: "POST",
69+
body: formData,
70+
// credentials: 'include', // Include cookies for CSRF token
71+
});
72+
if (!response.ok) {
73+
const errorData = await response.json().catch(() => ({}));
74+
throw new Error(`Upload failed: ${response.status} - ${JSON.stringify(errorData)}`);
75+
}
76+
const result = await response.json();
77+
if (!result.url) {
78+
throw new Error("No URL returned by the server");
79+
}
80+
return result.url;
81+
} catch (error) {
82+
console.error("Error while uploading the image:", error);
83+
throw error;
84+
}
85+
};
1786

18-
// Update the textarea before submitting the form
19-
const form = document.querySelector("#postForm");
20-
form.addEventListener("submit", (event) => {
21-
const markdownInput = document.querySelector("#markdownInput");
22-
markdownInput.value = crepe.getMarkdown(); // Retrieve modified Markdown content
23-
console.log(
24-
"Markdown content updated in textarea before form submission"
25-
);
26-
// Optionally, you can prevent the form submission for debugging purposes
27-
// event.preventDefault();
28-
// console.log("Form submission prevented for debugging");
29-
// console.log("Markdown content:", crepe.getMarkdown());
87+
const customUploader: Uploader = async (files, schema) => {
88+
console.log("Files received for upload:", files);
89+
const images: File[] = [];
90+
for (let i = 0; i < files.length; i++) {
91+
const file = files.item(i);
92+
if (file && file.type.includes("image")) {
93+
images.push(file);
94+
}
95+
}
96+
const nodes = await Promise.all(
97+
images.map(async (image) => {
98+
try {
99+
const src = await uploadImageToServer(image);
100+
const alt = image.name;
101+
console.log("Image uploaded:", src);
102+
return schema.nodes.image.createAndFill({ src, alt }) as Node;
103+
} catch (error) {
104+
console.error("Error uploading image:", error);
105+
return schema.nodes.image.createAndFill({
106+
src: "",
107+
alt: "Upload failed"
108+
}) as Node;
109+
}
110+
})
111+
);
112+
return nodes;
113+
};
114+
115+
document.addEventListener("DOMContentLoaded", () => {
116+
const editorElement = document.querySelector("#editor");
117+
if (!editorElement) {
118+
console.error("The #editor element is not found");
119+
return;
120+
}
121+
const markdownContent = editorElement.getAttribute("data-markdown") || "";
122+
const crepe = new Crepe({
123+
root: "#editor",
124+
defaultValue: markdownContent,
30125
});
31-
});
32-
});
126+
crepe.create()
127+
.then(() => {
128+
console.log("Editor instance created successfully");
129+
// Configure upload plugin
130+
crepe.editor.config((ctx) => {
131+
ctx.update(uploadConfig.key, (prev) => ({
132+
...prev,
133+
uploader: customUploader,
134+
}));
135+
});
136+
crepe.editor.use(upload);
137+
console.log("Milkdown editor initialized with upload plugin");
138+
const form = document.querySelector("#postForm");
139+
if (form) {
140+
form.addEventListener("submit", async (event) => {
141+
event.preventDefault();
142+
console.log("Form submission intercepted");
143+
const markdownInput = document.querySelector("#markdownInput") as HTMLTextAreaElement;
144+
if (!markdownInput) {
145+
console.error("The #markdownInput element is not found");
146+
return;
147+
}
148+
let markdownContent = crepe.getMarkdown();
149+
console.log("Original markdown content:", markdownContent);
150+
// Regular expression to find markdown images with blob URLs or base64
151+
const imageUrlRegex = /!\[.*?\]\((blob:[^)]+|data:image\/\w+;base64,[^)]+)\)/g;
152+
const imageMatches = [...markdownContent.matchAll(imageUrlRegex)];
153+
if (imageMatches.length === 0) {
154+
console.log("No embedded images found, submitting form directly");
155+
markdownInput.value = markdownContent;
156+
form.submit();
157+
return;
158+
}
159+
console.log(`Found ${imageMatches.length} embedded images to process`);
160+
try {
161+
// Create a copy of the markdown content to modify
162+
let updatedMarkdown = markdownContent;
163+
// Map to store already processed URLs (to avoid duplicates)
164+
const processedUrls = new Map<string, string>();
165+
for (const match of imageMatches) {
166+
const fullMatch = match[0];
167+
const imageUrl = match[1];
168+
// If we've already processed this URL, replace directly
169+
if (processedUrls.has(imageUrl)) {
170+
const serverUrl = processedUrls.get(imageUrl);
171+
updatedMarkdown = updatedMarkdown.replace(fullMatch, `![](${serverUrl})`);
172+
continue;
173+
}
174+
try {
175+
let serverImageUrl;
176+
let file;
177+
if (imageUrl.startsWith('blob:')) {
178+
console.log(`Processing blob URL: ${imageUrl}`);
179+
file = await blobUrlToFile(imageUrl);
180+
} else if (imageUrl.startsWith('data:image/')) {
181+
console.log(`Processing base64 image`);
182+
file = await base64ToFile(imageUrl);
183+
} else {
184+
// Not a blob or base64, skip
185+
continue;
186+
}
187+
// Upload the file to the server
188+
serverImageUrl = await uploadImageToServer(file);
189+
// Store the processed URL for subsequent occurrences
190+
processedUrls.set(imageUrl, serverImageUrl);
191+
// Replace the URL in the markdown
192+
updatedMarkdown = updatedMarkdown.replace(fullMatch, `![](${serverImageUrl})`);
193+
console.log(`Replaced embedded image with server URL: ${serverImageUrl}`);
194+
} catch (error) {
195+
console.error(`Error processing image ${imageUrl}:`, error);
196+
// Continue with other images even if one fails
197+
}
198+
}
199+
// Update the markdown content in the hidden field
200+
markdownInput.value = updatedMarkdown;
201+
console.log("Updated markdown content with server image URLs:", updatedMarkdown);
202+
// Submit the form
203+
form.submit();
204+
} catch (error) {
205+
console.error("Error processing images before form submission:", error);
206+
alert("An error occurred while processing images. Please try again.");
207+
}
208+
});
209+
} else {
210+
console.error("The #postForm is not found");
211+
}
212+
})
213+
.catch((error) => {
214+
console.error("Error initializing the editor:", error);
215+
if (document.querySelector("#editor")) {
216+
document.querySelector("#editor").innerHTML =
217+
`<div class="error">The editor could not be loaded. Error: ${error.message}</div>`;
218+
}
219+
});
220+
});

0 commit comments

Comments
 (0)