Open khayrullaev opened 3 years ago
Hey I am facing the same issue can you please tell me how did you handle this.
Hi! I have the same issue. any solution?
hello ! what was the solution for this?
Hey, guys actually I resolved this approach using another approach. I used a different editor it's called Tiny Editor.
In the below code you can see in the setup attribute I have implemented a logic using which we can do image delete from serve. But it works when we select the image by clicking on it and then press either the delete key or the backspace key.
import React , {Component, createRef} from 'react';
import { withRouter } from "react-router";
import axios from 'axios';
import FormData from "form-data";
import { v4 as uuidv4 } from "uuid";
import Aux from '../../hoc/_Aux';
import { Editor } from '@tinymce/tinymce-react';
import { EditorState } from 'draft-js';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
class FormTiny extends Component {
constructor(props){
super(props)
this.state = {
title: '',
category:'Coal',
content: EditorState.createEmpty(),
imageFolder: uuidv4(),
image: null,
imageUrl:null,
files:[]
}
this.imageRef = createRef();
this.editorRef = createRef();
}
addBlog=()=>{
if(this.state.title==="" || this.editorRef.current.getContent()==="" || this.state.image==null){
toast.warn("Please Fill All The Details");
}
else {
const formData = new FormData();
formData.append("blogImageId",uuidv4());
formData.append('category', this.state.category);
formData.append('thumbnail', this.state.image);
formData.append('thumbnailName', this.state.image.name);
formData.append('title', this.state.title );
formData.append("imageFolder", this.state.imageFolder);
formData.append('content', this.editorRef.current.getContent());
axios.post('/api/postBlog',formData)
.then(response =>{
toast.success("Your Blog Has Been Posted Successfully !!");
setTimeout(()=>{
this.props.history.push('/allBlog');
},2000);
})
.catch(error => {
console.log(error.response)
toast.error("Some Error Occured On Server Side");
});
}
}
handleQuillEditorChange = (content)=>{
this.setState({content: content})
}
thumbnailUploadHandeler = (event) =>{
this.setState({ image:event.target.files[0] })
this.setState({ imageUrl:URL.createObjectURL(event.target.files[0]) })
}
render(){
return(
<Aux>
<div style={{padding:"0"}} className="form-group input-group mb-3 col-md-3 col-sm-10">
<div className="input-group-prepend">
<label className="input-group-text" htmlFor="inputCategory">Category</label>
</div>
<select className="custom-select" id="inputCategory" value={this.state.category} onChange={(e)=>this.setState({category:e.target.value})}>
<option value="Coal">Coal</option>
<option value="Timber">Timber</option>
<option value="Salt">Salt</option>
</select>
</div>
<div className="form-group">
<label htmlFor="blogInputFile">Select Thumbnail</label>
<input ref={this.imageRef} type="file" className="form-control-file" onChange={this.thumbnailUploadHandeler} id="blogInputFile" aria-describedby="fileHelp" accept="image/gif, image/jpeg, image/png" required />
{this.state.imageUrl!=null?<img style={{margin:"1em 0"}} height="20%" width="20%" src={this.state.imageUrl} alt="Preview" />:""}
</div>
<div className="form-group">
<label htmlFor="inputTitle">Title</label>
<input type="text" value={this.state.title} onChange={(event)=>this.setState({title:(event.target.value)})} className="form-control" id="inputTitle" aria-describedby="postTitle" required/>
</div>
<label htmlFor="exampleInputPassword1">Content</label>
<Editor
apiKey={process.env.REACT_APP_TinyMCE_API_KEY}
onInit={(evt, editor) => this.editorRef.current = editor}
initialValue="<p>Start writing about port.</p>"
init={{
height: 500,
menubar: false,
image_dimensions: true,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help wordcount'
],
toolbar: 'undo redo | p h1 h2 blockquote | ' +
'bold italic underline strikethrough backcolor forecolor | image media table | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
setup: (ed) => {
ed.on('KeyDown', (e) => {
if ((e.keyCode === 8 || e.keyCode === 46) && ed.selection) { // delete & backspace keys
var selectedNode = ed.selection.getNode();
if (selectedNode && selectedNode.nodeName === 'IMG') {
var a = document.createElement('a');
a.href = selectedNode.src;
if(a.hostname===process.env.REACT_APP_IMAGE_HOSTNAME){
var imageName = selectedNode.src.split('/').slice(-1)[0];
const formData = {
imageFolder: this.state.imageFolder,
imageName: imageName
}
axios.post("/api/blogImageDelete",formData)
.then(response =>{
toast.success(response.data.msg);
})
.catch(error => {
console.log(error.response)
toast.error("Some Error Occured On Server Side");
});
}
}
}
});
},
relative_urls : false,
remove_script_host : false,
convert_urls : false,
images_upload_handler: (blobInfo, success, failure) => {
setTimeout(() => {
const formData = new FormData();
formData.append('imageFolder', this.state.imageFolder);
formData.append("imageId",uuidv4());
formData.append('blogAddedImage', blobInfo.blob());
axios.post("/api/blogImageUpload",formData)
.then(response =>{
success(process.env.REACT_APP_IMAGE_PRE_URL+"/blogContentImages/"+this.state.imageFolder+"/"+response.data.customFileName);
})
.catch(error => {
console.log(error.response)
failure('HTTP Error: ' + error.status, { remove: true });
toast.error("Some Error Occured On Server Side");
});
}, 1000);
},
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
}}
/>
<button style={{margin:"1em 0"}} type="submit" onClick={this.addBlog} className="btn btn-primary">Submit</button>
<button style={{margin:"1em 0 1em 1em"}} type="button" onClick={()=>{this.props.history.push('/allBlog')}} className="btn btn-danger">Cancel</button>
<ToastContainer toastClassName="colored-toast" autoClose={2000} hideProgressBar />
</Aux>
)
}
}
export default withRouter(FormTiny)
Maybe my method can help you.
im using custom imageHandler this is my code
///////
import { $_lib_fetchData } from '@/lib/commonUtils'
import React, { useEffect, useMemo, useRef } from 'react'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'
interface MyEditorPropsInterface {
setValue: React.Dispatch<React.SetStateAction<string>>
setImageFiles: React.Dispatch<React.SetStateAction<any>>
imageFiles: any[]
taskName: string
value: string
}
let groupCode = ''
const MyEditor = ({
setValue,
setImageFiles,
imageFiles,
taskName,
value
}: MyEditorPropsInterface) => {
const quillRef = useRef<any>()
const imageHandler = () => {
const input: any = document.createElement('input')
input.setAttribute('type', 'file')
input.setAttribute('accept', 'image/*')
input.setAttribute('multiple', '')
input.click()
input.addEventListener('change', async (e: any) => {
const files: File[] = [...input.files]
const formData: FormData = new FormData()
if (groupCode !== '') {
formData.append('fileGroupCode', groupCode)
}
if (files.length > 0) {
files.forEach((file: any) => formData.append('files', file))
}
formData.append('taskName', taskName)
const res = await $_lib_fetchData({
url: '/files',
method: 'post',
params: formData
})
if (res.data) {
groupCode = res.data.fileGroupCode
res.data.fileIds.forEach((fileId: any) => {
const editor = quillRef.current.getEditor()
const range = editor.getSelection()
const src = `http://yourUrl../${fileId}`
editor.insertEmbed(range.index, 'image', src)
setImageFiles((prev: any) => [...prev, { path: src, id: fileId }])
})
}
})
}
const deleteImage = async (fileId: string) => {
const res = await $_lib_fetchData({
url: `/files/${fileId}`,
method: 'delete',
params: {}
})
}
const modules = useMemo(
() => ({
toolbar: {
container: [
[{ font: [] }],
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[
{ list: 'ordered' },
{ list: 'bullet' },
{ indent: '-1' },
{ indent: '+1' }
],
[{ align: [] }, { color: [] }, { background: [] }],
['image', 'link', 'video']
],
handlers: { image: imageHandler }
},
clipboard: {
matchVisual: false
}
}),
[]
)
const formats = [
'font',
'header',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'list',
'bullet',
'indent',
'link',
'image',
'video',
'align',
'color',
'background'
]
useEffect(() => {
if (
quillRef.current?.lastDeltaChangeSet?.ops[1]?.delete === 1 &&
imageFiles.length > 0
) {
for (let index = 0; index < imageFiles.length; index++) {
if (!quillRef.current?.value.includes(imageFiles[index].path)) {
const tempImageFiles = structuredClone(imageFiles)
const filteredIamgeFiles = tempImageFiles.filter(
(image: any) => image.id !== imageFiles[index].id
)
deleteImage(imageFiles[index].id)
setImageFiles(filteredIamgeFiles)
}
}
}
}, [quillRef.current?.lastDeltaChangeSet?.ops[1]?.delete])
return (
<>
<ReactQuill
ref={quillRef}
style={{ height: '400px' }}
modules={modules}
formats={formats}
theme='snow'
value={value}
onChange={setValue}
/>
</>
)
}
export default MyEditor
///////
As you can see from the code, I uploaded a file to the database every time I uploaded an image.
After that, I updated the src value and id value received as a response to the arbitrary state I created.
Then, we sent a request to delete the image in the DB by detecting the deletion using the lastDeltaChangeSet property of react-quill and useEffect.
Try modifying my code to suit your own code.
Developers who have additional, better methods, please reply.
great solution
hi @Joji6666 , i used ur code above, but I got error when using 'setImageFiles' and the editor disappears, Error: 'addRange(): The given range isn't in document.' it seems that it doesn't allow me to set value in the image handler.
Could it be that your development environment is different from mine? I am using the latest version of NextJS and react-quill is using version 2.0.0. Sorry for the late reply.
I am using react-quill with quill-image-uploader. I want to get the url when an image is deleted to also delete it from my storage. Is there any way to achieve this?
react-quill version 1.3.5