Created
October 4, 2024 14:12
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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