Skip to content

Instantly share code, notes, and snippets.

@bohwaz
Created October 4, 2024 14:12
Show Gist options
  • Save bohwaz/a78291f3ab92a2161de566fcca975e0e to your computer and use it in GitHub Desktop.
Save bohwaz/a78291f3ab92a2161de566fcca975e0e to your computer and use it in GitHub Desktop.
Insert images in a MarkDown textarea, and resize them before upload, with no specific handling on the server side
<!DOCTYPE html>
<html>
<head>
<title>Javascript insert images into markdown with resize before upload</title>
</head>
<body>
<pre><h2>This demo shows how you can easily add images to a markdown textarea</h2>
You can drag and drop, paste or select images. Files other than images are discarded.
The images are resized on the client side and inserted as Markdown inside the text.
When the form is submitted, images that have been removed from the textarea are discarded.
Others are sent along the form, as regular file uploads.
You don't need anything specific on the server side.
License: public domain (C) BohwaZ</pre>
<form method="post" action="" enctype="multipart/form-data" data-max-image-size="800">
<fieldset>
<legend>Markdown text</legend>
<p><textarea name="text" cols="80" rows="30"></textarea></p>
<p><input type="file" names="images[]" accept=".jpg,.gif,.jpeg,.jpe,.webp,.svg" multiple="multiple" /></p>
</fieldset>
<p><input type="submit" /></p>
</form>
<script>
const textarea = document.querySelector('textarea');
const input = document.querySelector('input[type="file"]');
const form = input.form;
const max_size = parseInt(form.dataset.maxImageSize, 10);
const files = {};
// Hide real file input
input.style.display = 'none';
var button = document.createElement('button');
button.type = 'button';
button.innerText = 'Insert image';
button.onclick = () => input.click();
input.parentNode.appendChild(button);
// Regular logic for file selector
input.onchange = () => addFiles(input.files);
// Drag and drop code
const drag = (e) => {
e.stopPropagation();
e.preventDefault();
};
textarea.addEventListener('dragenter', drag, false);
textarea.addEventListener('dragover', drag, false);
textarea.addEventListener('drop', (e) => {
e.stopPropagation();
e.preventDefault();
const files = [...e.dataTransfer.items].map(item => item.getAsFile());
addFiles(files);
}, false);
// Paste image directly
window.addEventListener('paste', (e) => {
const files = [...e.clipboardData.items]
.map(item => item.getAsFile())
.filter(f => f !== null); // Remove null values (non-files)
if (!files.length) {
// Don't prevent pasting text
return;
}
e.preventDefault();
addFiles(files);
});
// Before submit, make sure files are still present in text, then put them in form
form.onsubmit = (e) => {
if (form.inert) {
// Upload in progress
e.preventDefault();
return false;
}
const dt = new DataTransfer;
for (var name in files) {
if (!files.hasOwnProperty(name)) {
continue;
}
let find_text = '](' + name + ')';
// Image has been deleted from text, don't upload
if (textarea.value.indexOf(find_text) === -1) {
continue;
}
let file = files[name];
dt.items.add(new File([file], name, {type: file.type}));
}
// Replace input[type=file] files with new list of files
input.files = dt.files;
// Disable form while upload takes place
form.inert = true;
// Remove the following 3 lines to proceed with upload
alert(`I will upload ${input.files.length} files, but form sending is disabled in this demo`);
e.preventDefault();
return false;
};
async function addFiles(list)
{
for (var i = 0; i < list.length; i++) {
await addFile(list[i]);
}
}
// Add file, if it's an image
async function addFile(file)
{
// Ignore non-images
if (!file || !file.type.match(/^image\/(jpeg|gif|png|webp|svg\+xml)$/)) {
return;
}
// Filter file name
var name = file.name.replace(/[^a-zA-Z0-9_.\-]+/, '_').toLowerCase();
// Just in case the file name is now empty
if (name.match(/^_*$/)) {
name = 'img-' + +(new Date);
}
// All images are exported as WebP in the end
name = name.replace(/\.[^.]*$|$/, '.webp');
// Insert markdown code
insertText(textarea, '![](' + name + ')');
// Resize image
const blob = await resizeImage(file, max_size, null, 'medium');
// Overwrite file object with resized image
file = new File([blob], name, {type: 'image/webp'});
// Store for later
files[name] = file;
}
// Insert text in textarea
function insertText(el, text)
{
el.setRangeText(text, el.selectionEnd, el.selectionEnd, 'select');
}
/**
* Calculate new image width / height from a given image.
* If height is NULL, then the final image size will use the "width" parameter as max width or height.
* The other dimension will be calculated, maintaining the aspect ratio.
*/
function getNewImageSize(img, width, height = null)
{
if (height === null) {
if (img.width > img.height) {
height = Math.round(img.height * width / img.width)
}
else if (img.width == img.height) {
height = width;
}
else {
height = width;
width = Math.round(img.width * height / img.height);
}
if (img.width < width && img.height < height)
{
width = img.width, height = img.height;
}
}
width = Math.abs(width);
height = Math.abs(height);
return {width, height};
}
/**
* Resize a File object. Returns a promise. On success this promise will return a Blob object.
* Use FileReader.readAsDataURL to obtain a data URI from this blob.
*/
function resizeImage(file, max_width, max_height = null, quality = 'low')
{
if (typeof OffscreenCanvas === 'undefined') {
throw new Error('Your browser is unsupported');
}
if (!(file instanceof File)) {
throw new Error('Invalid file argument');
}
if (!file.type.match(/^image\//)) {
throw new Error('This file is not an image');
}
var url = (window.URL || window.webkitURL).createObjectURL(file);
return new Promise((resolve, reject) => {
let img = new Image;
img.onload = () => {
let n = getNewImageSize(img, max_width, max_height);
const canvas = new OffscreenCanvas(n.width, n.height);
canvas.imageSmoothingEnabled = true;
const context = canvas.getContext('2d');
context.imageSmoothingQuality = quality;
context.drawImage(img, 0, 0, img.width, img.height, 0, 0, n.width, n.height);
const o = {type: "image/webp", quality: 0.75};
const blob = canvas.convertToBlob(o).then((blob) => resolve(blob));
};
img.onerror = reject;
img.src = url;
});
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment