Skip to content

Instantly share code, notes, and snippets.

@DavidWells
Created October 6, 2025 20:07
Show Gist options
  • Select an option

  • Save DavidWells/1256073266b1ecc98de9dfaaf27199ab to your computer and use it in GitHub Desktop.

Select an option

Save DavidWells/1256073266b1ecc98de9dfaaf27199ab to your computer and use it in GitHub Desktop.
Upload image to textarea like github
import React, { useCallback, useEffect, useState, useRef } from 'react'
// function uploadFile(file) {
// return new Promise((resolve) => {
// setTimeout(() => {
// resolve(`https://via.placeholder.com/150?text=${file.name}`)
// }, 1000)
// })
// }
const uploadFile = async (file) => {
const filename = encodeURIComponent(file.name);
const type = encodeURIComponent(file.type);
const res = await fetch(
`https://mirri.link/api/get-url?filename=${filename}&contentType=${type}`
);
const data = await res.json();
const { url, publicUrl } = data;
const uploadFileResult = await fetch(url, {
method: "PUT",
body: file,
});
if (!uploadFileResult.ok) {
return undefined;
}
return publicUrl;
};
const cn = (...classNames) => classNames.filter(Boolean).join(' ')
const insertAtCaret = (el, text) => {
if (el.selectionStart || el.selectionStart == '0') {
el.value = el.value.substring(0, el.selectionStart) + text + el.value.substring(el.selectionEnd, el.value.length)
} else {
el.value += text
}
}
const autoGrow = (e) => {
const el = e.target
el.style.height = 'auto'
el.style.height = `${el.scrollHeight + 5}px`
}
const handleUpload = async (e, file) => {
const placeholder = `![Uploading ${file.name}...]()`
insertAtCaret(e.target, placeholder)
const url = await uploadFile(file)
const isImage = !file.type || file.type?.startsWith('image')
e.target.value = e.target.value.replace(placeholder, `${isImage ? '!' : ''}[${file.name}](${url})`)
}
const TextArea = ({ onChange, value = '', className = '', ...props }) => {
const ref = useRef()
const [preview, setPreview] = useState(false)
const [previewContent, setPreviewContent] = useState('')
const paste = useCallback(async (e) => {
if (!e.clipboardData.files.length) return
e.preventDefault()
const file = e.clipboardData.files[0]
await handleUpload(e, file)
change(e)
})
const drop = useCallback(async (e) => {
if (!e.dataTransfer.files.length) return
e.preventDefault()
const file = e.dataTransfer.files[0]
await handleUpload(e, file)
change(e)
})
const change = useCallback((e) => {
if (onChange) onChange(e)
setPreviewContent(e.target.value)
}, [])
useEffect(() => {
setPreviewContent(value)
}, [value])
useEffect(() => {
if (!ref.current) return
ref.current.addEventListener('paste', paste)
ref.current.addEventListener('drop', drop)
ref.current.addEventListener('input', autoGrow)
autoGrow({ target: ref.current })
return () => {
ref.current?.removeEventListener('paste', paste)
ref.current?.removeEventListener('drop', drop)
ref.current?.removeEventListener('input', autoGrow)
}
}, [ref.current])
return (
<div
className={cn(
'rounded-md border border-gray-300 focus-within:border-blue-600 focus-within:ring-1 focus-within:ring-blue-600',
className,
)}
>
{preview ? (
<div
className='min-h-32 p-3.5 px-4 mb-3 prose prose-sm text-gray-900 activity-content'
dangerouslySetInnerHTML={{ __html: previewContent }}
/>
) : (
<textarea
{...props}
className={cn(
'min-h-32 w-full flex-1 text-sm leading-[1.75] rounded-md bg-transparent p-3.5 px-4 border-0 focus:ring-0 resize-none',
className,
)}
onChange={change}
value={value}
ref={ref}
/>
)}
<div className='flex justify-end items-center p-3 px-3.5 pt-1 gap-2 text-[11px] text-gray-500'>
<div
className={cn(
'size-4 transition cursor-pointer transition',
preview ? 'text-blue-600' : 'text-gray-500 hover:text-gray-900',
)}
title='Toggle preview'
onClick={() => setPreview(!preview)}
/>
</div>
</div>
)
}
export default TextArea
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment