Created
October 6, 2025 20:07
-
-
Save DavidWells/1256073266b1ecc98de9dfaaf27199ab to your computer and use it in GitHub Desktop.
Upload image to textarea like github
This file contains hidden or 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
| 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